Русский
Русский
English
Статистика
Реклама

Wordpress

Новый плагин CrowdSec для защиты сайтов на WordPress

17.02.2021 12:10:47 | Автор: admin

Всем привет! Мы активно работаем над развитием нашей системы блокировки нежелательных IP-адресов и сегодня рады рассказать сообществу о нашей новой разработке плагине WordPress для упрощения жизни веб-мастеров и защиты администрируемых ими сайтов.

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

Новый баунсер CrowdSec для WordPress решает сразу две задачи веб-мастера: отсекает нежелательные IP-адреса и имеет дополнительную функцию более мягкой блокировки показывает нежелательным или подозрительным IP-адресам капчу.

Для того, чтобы плагин работал, вам, как и в случае отдельно взятого сервера или сети, нужно будет установить серверную часть CrowdSec. Ранее мы уже публиковали небольшой туториал на эту тему, почитать можно тут. Наш плагин для WordPress совместим с версиями CrowdSec 1.0.х, то есть с последними актуальными релизами после нашего большого обновления, в котором мы провели рефакторинг архитектуры и перешли на использование API вместо прямых обращений элементов системы к БД.

Flex Mode

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

CrowdSec достаточно мощный инструмент, как и любой другой проект в этой сфере, а перманентный бан по IP-адресу не шутка.

Именно для того, чтобы веб-мастер по незнанию или не внимательности не сделал непоправимое, мы создали Flex Mode для плагина WordPress.

Flex Mode это щадящий режим работы баунсера и сервера с сайтом. В этом режиме владелец подозрительного IP-адреса не может получить перманентный бан. Максимум ограничений доступа к сайту для такого посетителя заключается в необходимости пройти капчу.

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

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

А вот так уже кастомизированная:

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

Баланс защиты и производительности

У нашего плагина есть два режима поддержания связи с серверной частью CrowdSec.

Первый режим по умолчанию "Live mode". Это режим, в котором баунсер при обращении к сайту нового пользователя в режиме реального времени обращается к API сервера для получения информации. В том числе, в этом режиме, IP посетителя сайта проверяется и по общему бан-листу CrowdSec, который генерируется с нашей стороны как разработчиков. После этого сервер передает информацию обратно баунсеру в плагине и он уже банит/не банит пользователя, или показывает ему капчу.

Этот мод обеспечивает наибольшую защиту вашего сайта, но, как можно понять, влияет на производительность сайта и, при большом трафике, создает нагрузку на серверную часть CrowdSec, генерируя поток трафика.

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

Он называется "Stream Mode" и работает по обратному принципу, нежели "Live Mode". При включении режима поточной работы, главным инициатором выгрузки бан-листа становится серверная часть, которая с определенной периодичностью просто обновляет списки на стороне баунсера, позволяя тому работать с большей автономией.

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

Если вы используете CDN, обратный прокси или балансировщик нагрузки, вы сможете указать в настройках баунсера диапазоны IP-адресов этих устройств. Это позволит вам проверять IP-адреса ваших пользователей. Для других IP-адресов баунсер не будет доверять заголовку X-Forwarded-For.

Что будет дальше

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

Новый плагин мы протестировали на большинстве версий WordPress, которые сейчас актуальны в мире. В общей сложности, тестами мы покрыли 90%, если верить статистике использования WP по миру. Также мы проводили тестирование на PHP версий 7.2, 7.3, 7.4 и 8, чтобы исключить конфликты на уровне языка, на котором написан WordPress. Само собой, с выходом новых версий как движка, так и языка программирование, мы продолжим работу в этом направлении.

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

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

Подробнее..

TWAPT пентестим по-белому в домашних условиях

14.04.2021 14:22:42 | Автор: admin

Чтобы поймать преступника, ты должен думать, как преступник, ты должен чувствовать, как преступник, ты должен сам стать преступником!

Подобную фразу можно встретить во многих детективных фильмах или триллерах, где защитники правопорядка пытаются поймать неуловимого злодея. В эпоху Интернета теми самыми неуловимыми злодеями можно назвать киберпреступников (хакеров), которым для совершения преступлений не требуется показывать своего лица, и даже не обязательно находиться в одной стране с жертвой атаки, а все их действия могут остаться анонимными. Чтобы понять как действует злоумышленником, нужно самому стать им. Однако, у Уголовного Кодекса РФ на это другие взгляды. В частности, 28 глава УК РФ регулирует преступления в сфере компьютерной информации. Как быть в этом случае специалисту по информационной безопасности, если нарушать законодательство - плохая идея, а понять как мыслит злоумышленник все же необходимо? На помощь приходят бесплатные площадки тестирования на проникновение, где любой желающий может попробовать свои силы в пентесте, прокачать собственные навыки и использовать полученные знания для повышения уровня защищенности своей компании. Есть 2 типа площадок:

  • онлайн;

  • оффлайн.

К онлайн относят довольно популярные ресурсы: Test lab от Pentestit, hackthebox, pentesterlab, Root me и многие другие. Все эти площадки довольно популярные и останавливаться на них мы не будем. Сегодня мы поговорим про оффлайн площадку TWAPT.

Статья носит информационный характер. Не нарушайте законодательство.

TWAPT

Домашняя страница

Сам по себе проект задумывался как сборник нескольких оффлайн платформ для тестирования на проникновение.

Весь набор представлен в виде Docker-контейнеров, которые можно просто скачать и запустить за "пару кликов". Но перед этим необходимо установить (на примере Debian 10):

Docker
# apt update# apt install apt-transport-https ca-certificates curl gnupg lsb-release# curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg# echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null# apt update# apt install docker-ce docker-ce-cli containerd.io
Docker-composer
# apt update# curl -L "https://github.com/docker/compose/releases/download/1.29.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose# chmod +x /usr/local/bin/docker-compose# docker-compose --version

Скачивание и запуск контейнеров происходит командами:

# git clone https://github.com/MoisesTapia/TWAPT# cd TWAPT# docker-compose up -d

Чтобы узнать какой порт заняло то или иное веб-приложение нужно выполнить команду:

# docker-compose ps

Вероятно, предполагалось, что от пользователя кроме запуска контейнера больше нчиего не потребуется, но в реальности некоторые контейнеры после запуска требуют дополнительных манипуляций. Например, после запуска контейнера с bWAPP требуется перейти по адресу localhost:8082/install.php и произвести установку БД для корректной работы, после этого можно продолжать пользоваться. А в случае с VulnWordpress нужно скрипт start.sh запустить самостоятельно внутри контейнера. Для этого необходимо:

  • Узнать ID контейнера:

# docker ps -a
  • Запустить оболочку контейнера:

# docker exec -ti /ID контейнера/ bash -c "/bin/bash"
  • Самостоятельно выполнить script.sh.

После этого можно произвести установку WordPress.

Состав набора

Mutillidae

Mutillidae - язвимое веб-приложение, поддерживаемое организацией OWASP, которое включает в себя порядка 40 различных веб-уязвимостей, которые актуальны для OWASP Top Ten 2007, 2010, 2013 и 2017. На сайте присутствуют подсказки для прохождения заданий, а если приложение стало некорректно работать, то можно кнопкой "Reset DB" сбросить настройки БД и восстановить работоспособность веб-приложения. Такая же функция есть почти у всех ресурсов, которые представлены ниже.

bWAPP

bWAPP (buggy web application) - это бесплатное, намеренно небезопасное веб-приложение с открытым исходным кодом. Тоже охватывает все уязвимости, присутствующие в OWASP Top Ten.

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

  • уровень Low;

    На первом уровне для перебора требуется только ввести логин и пароль.

  • уровень Medium;

    Уже сложнее, для каждого запроса используется собственный salt, который обновляется.

  • уровень High.

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

WebGoat

WebGoat - это заведомо небезопасное приложение, которое позволяет тестировать уязвимости, обычно обнаруживаемые в приложениях, написанные на Java и использующих популярные компоненты с открытым исходным кодом.

DVWAP

DVWAP - уязвимое веб-приложение, написанное на PHP и использующее MySQL в качестве базы данных. Среди уязвимостей, которые представлены в веб-приложении, собраны множество типов инъекций, XSS, LFI/RFI, уязвимости капчи и т.д. Также присутствует 3 уровня сложности, но в отличие от того же bWAPP есть четвертый уровень - impossible, где невозможно проэксплуатировать уязвимость и разработчики дают краткое пояснение почему. После первого запуска контейнера необходимо нажать кнопку "Setup/Reset DB" для установки базы данных.

Bricks

Bricks - еще одно веб-приложение, написанное на PHP. Из набора уязвимостей, которые используются в составе платформы: различные уязвимости страницы аутентификации, формы загрузки файлов, а также SQL-инъекции. Из плюсов можно назвать подсказку по команде, которую для наглядности формирует и отправляет веб-приложение к базе данных. Поэтому можно смотреть какой запрос был отправлен, корректен ли он и как его можно изменить. В целом, набор заложенных уязвимостей немного уступает тем же bWAPP или DVWAP.

Juice-Shop

Juice-Shop - уязвимое веб-приложение, написанное на Node.js, Express и Angular и представляет из себя типичный пример интернет-магазина с 1-2 присущими ему уязвимостями, перечисленными в OWASP Top Ten, такими как Injection, XSS, Broken Authentication, Broken Access Control, Sensitive Data Exposure и т.д.

NinjaWeb

NinjaWeb, как и на всех предыдущих платформах, использует уязвимости, находщиеся в OWASP Top Ten. Приятным бонусом для данной платформы также стало наличие подсказок и полноценное прохождение задания в случае затруднений, не требующее от пользователя искать прохождение на сторонних ресурсах.

VulnWordPress

VulnWordPress - чистый CMS WordPress, где можно установить уязвимый компонент и проверить наличие уязвимости.

Тестирование WAF

Заведомо уязвимые веб-приложения хороши еще и тем, что на их примере можно оценить работу средств защиты, например, WAF. Достаточно на локальном сервере развернуть платформу, а в случае с Docker-контейнером это сделать достаточно просто, и настроить работу WAF на уязвимое веб-приложение. Теперь мы не просто изучаем как эксплуатируются веб-уязвимости, но и пробуем обойти средства защиты, чтобы еще больше приблизиться к реальным условиям, с которыми сталкивается злоумышленник при атаках.

Попробуем протестировать веб-приложение DVWAP, защитив его с помощью Nemesida WAF Free (бесплатная версия), который обеспечивает защиту на основе сигнатурного анализа. Для сегодняшнего тестирования этого будет достаточно. Но у сигнатурного анализа, несмотря на все его преимущества, есть недостатки, например, база сигнатур находится в открытомдоступеи злоумышленник, используя ее, может составить запрос, который позволит обойти защиту. Тем не менее, если база сигнатур составлена качественно, то и уровень защиты веб-приложения будет высоким.

Итак, протестируем один из самых популярных классов уязвимостей согласно OWASP Top Ten - инъекции.

Command injection

Пробуем подставить команду в поле для проверки доступности сервера:

127.0.0.1; ls /

От данной атаки Nemesida WAF Free не смог защитить. Тогда попробуем прочитать файл /etc/passwd.

2021/04/13 10:57:23 [error] 6261#6261: *11 Nemesida WAF: the request 8c137c6199bfdb63851c0c7b15468897 blocked by rule ID 1559 in zone BODY, client: 192.168.0.135, server: dvwa.site.lan, request: "POST /vulnerabilities/exec/ HTTP/1.1", host: "dvwa.site.lan", referrer: "http://dvwa.site.lan/vulnerabilities/exec/"

В error.log видим блокировку по правилу 1559. Обращаемся к базе сигнатур и смотрим сигнатуру, по которой был заблокирован запрос:

В первом случае запрос не был заблокирован, но это сделано для того, чтобы веб-приложение могло нормально функционировать т.к. в противном случае любой URL будет блокироваться из-за знака "/". Во втором случае на основе сигнатур запрос был заблокирован.

Попробуем обойти блокировку, для этого воспользуемся инструментом Commix.

# python commix.py -u http://dvwa.site.lan/vulnerabilities/exec/ -d "ip=127.0.0.1&Submit=Submit" -p "ip"

В результате все атаки были заблокированы.

SQLi

Теперь тестируем более известный вид инъекций, защитив уязвимое веб-приложение Nemesida WAF Free.

# sqlmap -u 'http://dvwa.site.lan/vulnerabilities/sqli/?id=1*&Submit=Submit'  --dbs --random-agent

Все попытки эксплуатации уязвимости заблокированы:

Попробуем добавить скрипты для обфускации пейлоадов, доступные в SQLmap. Но это не принесло результатов:

XSS

Последними в списке будут не менее популярные XSS. Для тестирования воспользуемся инструментом XSStrike:

# python3 xsstrike.py -u 'http://dvwa.site.lan/vulnerabilities/xss_r/?name='

В стандартной конфигурации Nemesida WAF Free заблокировал все атаки:

Вывод

TWAPT однозначно отличная площадка для проверки и оттачивания навыков в области практической информационной безопасности, которая позволяет специалисту тренироваться и развиваться не нарушая законодательство. К тому же, метод защиты заведомо уязвимых стендов для проверки работы WAF, пожалуй, один из лучших способов его тестирования и при грамотном составлении базы сигнатур вероятность эксплуатации уязвимостей заметно сокращается. Методы атак веб-приложений и обхода средств защиты с каждым днем развиваются. Так или иначе, когда-нибудь сигнатурный анализ уже будет не так эффективен как сейчас. Однако, в Nemesida WAF используется модуль машинного обучения Nemesida AI, который за счет использования поведенческих моделей, построенных на основе запросов к конкретному веб-приложению, позволяет значительно повысить точность выявления атак с минимальным количеством ложных срабатываний. Также реализован механизм выявления ботов, смс-флуда, защиты от атак методом пебора паролей

Подробнее..

Модульные frond-end блоки пишем свой пакет. Часть 2

20.05.2021 14:22:33 | Автор: admin

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

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

Предисловие

Представлюсь - я молодой веб разработчик с опытом работы 5 лет. Крайний год я работаю на фрилансе и большая часть текущих проектов связана с WordPress. Несмотря на различую критику CMS в общем и WordPress в часности, я считаю сама архитектура WordPress это довольно удачное решение, хотя конечно не без определенных недостатков. И один из них на мой взгляд это шаблоны. В крайних обновлениях сделаны большие шаги чтобы это исправить, и Gutenberg в целом становится мощным инструментом, однако к сожалению в большинстве тем продолжается каша в шаблонах, стилях и скриптах, которая делает редактирование чего-либо крайне болезненным, а переиспользование кода зачастую невозможным. Именно эта проблема и подтолкнуло меня к идее своего пакета, который бы организовывал структуру и позволял переиспользовать блоки.

Реализация будет в виде composer пакета, который можно будет использовать в совершенно различных проектах, без привязки к WordPress.

Постановка задачи

Понятие блок ниже будет по сути тем же понятием что и блок в BEM методологии, т.е. это будет группа html/js/css кода которая будет представлять одну сущность.

Генерировать html и управлять зависимостями блоков мы будем через php, что говорит о том, что наш пакет будет подходить для проектов с бекендом на php. Также условимся на берегу что, не вдаваясь в споры, не будем поддаваться влиянию новомодных вещей, таких как css-in-js или bem-json и будем придерживаться эль-классико классического подхода, т.е. предполагать что html, css и js это разные файлы.

Теперь давайте сформулируем наши основные требования к пакету:

  • Обеспечить структуру блоков

  • Предоставить поддержку наследования (расширения) блоков

  • Предоставить возможность использовать блок в блоке и соответственно поддержку зависимости ресурсов одного блока от ресурсов других блоков

Структура пакета

О ресурах блока и twig шаблонах

Как условились выше, такие ресурсы как css и js всегда будут в виде обычных файлов, т.е. это будут .js и .css или .min.css и .min.js в случае использования препроцесссоров и сборщиков (как webpack например). Для вставки данных в html код мы будем использовать шаблонизатор Twig (для тех кто не знаком ссылка). Кто-то может заметить, что php и сам по себе хороший шаблонизатор, не будем вдаваться в споры, кроме доводов указанных на главной странице проекта Twig, отмечу важный для меня пункт, то что он дисциплинирует, т.е. заставляет отделять обработку от вывода и подготавливать переменные заранее, и в данном случае мы будем использовать его.

  1. Блок

    Каждый блок будет состоять из:

    1. Статических ресурсов (css/js/twig)

    2. Класса блока, который будет предоставлять данные для twig шаблона и управлять зависимостями.

  2. Вспомогательные классы: Settings (пути к блокам и их пространства имен), TwigWrapper (обертка для Twig пакета), BlocksLoader (автозагрузка всех блоков, опционально), Helper (набор статических доп. функций)

  3. Renderer класс - связующий класс, который будет объединять вспомогательные классы, предоставлять функцию рендера блока, содержать список использованных блоков и их ресурсы (css, js)

Требования к блокам

В отличии от первого пакета количество требований сократилось, теперь это:

  • php 7.4

  • Классы блоков должны иметь PSR-4 совместимое пространство имен с автозагрузчиком (PSR-4 де факто стандарт, если вы используете автозагрузчик от composer, т.е. указываете autoload/psr4 директиву в вашем composer.json то ваш проект уже соответствует этому требованию)

  • Имена ресурсов должны совпадать с именем блока (например для Button.php будут Button.css и Button.twig)

Реализация

Ниже части реализации (классы) будут в формате : текстовое описание, код реализации и код тестов.

Block

Основной действующий класс, его потомки будут содержать данные для twig шаблона (в protected полях) и предоставлять список зависимостей, а также мы сможем получить путь к ресурсам (шаблону, стилям). Все наша магия при работе с полями будет строится на функции get_class_vars которая предоставит имена полей класса и на ReflectionProperty классе, который предоставит информацию об этих полях, такую как видимость поля (protected/public) и его тип. Мы будем собирать информацию только о protected полях.

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

Block.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks;use Exception;use ReflectionProperty;abstract class Block{    public const TEMPLATE_KEY_NAMESPACE = '_namespace';    public const TEMPLATE_KEY_TEMPLATE = '_template';    public const TEMPLATE_KEY_IS_LOADED = '_isLoaded';    public const RESOURCE_KEY_NAMESPACE = 'namespace';    public const RESOURCE_KEY_FOLDER = 'folder';    public const RESOURCE_KEY_RELATIVE_RESOURCE_PATH = 'relativeResourcePath';    public const RESOURCE_KEY_RELATIVE_BLOCK_PATH = 'relativeBlockPath';    public const RESOURCE_KEY_RESOURCE_NAME = 'resourceName';    private array $fieldsInfo;    private bool $isLoaded;    public function __construct()    {        $this->fieldsInfo = [];        $this->isLoaded   = false;        $this->readFieldsInfo();        $this->autoInitFields();    }    public static function onLoad()    {    }    public static function getResourceInfo(Settings $settings, string $blockClass = ''): ?array    {        // using static for child support        $blockClass = ! $blockClass ?            static::class :            $blockClass;        // e.g. $blockClass = Namespace/Example/Theme/Main/ExampleThemeMain        $resourceInfo = [            self::RESOURCE_KEY_NAMESPACE              => '',            self::RESOURCE_KEY_FOLDER                 => '',            self::RESOURCE_KEY_RELATIVE_RESOURCE_PATH => '',// e.g. Example/Theme/Main/ExampleThemeMain            self::RESOURCE_KEY_RELATIVE_BLOCK_PATH    => '',// e.g. Example/Theme/Main            self::RESOURCE_KEY_RESOURCE_NAME          => '',// e.g. ExampleThemeMain        ];        $blockFolderInfo = $settings->getBlockFolderInfoByBlockClass($blockClass);        if (! $blockFolderInfo) {            $settings->callErrorCallback(                [                    'error'      => 'Block has the non registered namespace',                    'blockClass' => $blockClass,                ]            );            return null;        }        $resourceInfo[self::RESOURCE_KEY_NAMESPACE] = $blockFolderInfo['namespace'];        $resourceInfo[self::RESOURCE_KEY_FOLDER]    = $blockFolderInfo['folder'];        //  e.g. Example/Theme/Main/ExampleThemeMain        $relativeBlockNamespace = str_replace($resourceInfo[self::RESOURCE_KEY_NAMESPACE] . '\\', '', $blockClass);        // e.g. ExampleThemeMain        $blockName = explode('\\', $relativeBlockNamespace);        $blockName = $blockName[count($blockName) - 1];        // e.g. Example/Theme/Main        $relativePath = explode('\\', $relativeBlockNamespace);        $relativePath = array_slice($relativePath, 0, count($relativePath) - 1);        $relativePath = implode(DIRECTORY_SEPARATOR, $relativePath);        $resourceInfo[self::RESOURCE_KEY_RELATIVE_RESOURCE_PATH] = $relativePath . DIRECTORY_SEPARATOR . $blockName;        $resourceInfo[self::RESOURCE_KEY_RELATIVE_BLOCK_PATH]    = $relativePath;        $resourceInfo[self::RESOURCE_KEY_RESOURCE_NAME]          = $blockName;        return $resourceInfo;    }    private static function getResourceInfoForTwigTemplate(Settings $settings, string $blockClass): ?array    {        $resourceInfo = self::getResourceInfo($settings, $blockClass);        if (! $resourceInfo) {            return null;        }        $absTwigPath = implode(            '',            [                $resourceInfo['folder'],                DIRECTORY_SEPARATOR,                $resourceInfo['relativeResourcePath'],                $settings->getTwigExtension(),            ]        );        if (! is_file($absTwigPath)) {            $parentClass = get_parent_class($blockClass);            if ($parentClass &&                is_subclass_of($parentClass, self::class) &&                self::class !== $parentClass) {                return self::getResourceInfoForTwigTemplate($settings, $parentClass);            } else {                return null;            }        }        return $resourceInfo;    }    final public function getFieldsInfo(): array    {        return $this->fieldsInfo;    }    final public function isLoaded(): bool    {        return $this->isLoaded;    }    private function getBlockField(string $fieldName): ?Block    {        $block      = null;        $fieldsInfo = $this->fieldsInfo;        if (key_exists($fieldName, $fieldsInfo)) {            $block = $this->{$fieldName};            // prevent possible recursion by a mistake (if someone will create a field with self)            // using static for children support            $block = ($block &&                      $block instanceof Block &&                      get_class($block) !== static::class) ?                $block :                null;        }        return $block;    }    public function getDependencies(string $sourceClass = ''): array    {        $dependencyClasses = [];        $fieldsInfo        = $this->fieldsInfo;        foreach ($fieldsInfo as $fieldName => $fieldType) {            $dependencyBlock = $this->getBlockField($fieldName);            if (! $dependencyBlock) {                continue;            }            $dependencyClass = get_class($dependencyBlock);            // 1. prevent the possible permanent recursion            // 2. add only unique elements, because several fields can have the same type            if (                ($sourceClass && $dependencyClass === $sourceClass) ||                in_array($dependencyClass, $dependencyClasses, true)            ) {                continue;            }            // used static for child support            $subDependencies = $dependencyBlock->getDependencies(static::class);            // only unique elements            $subDependencies = array_diff($subDependencies, $dependencyClasses);            // sub dependencies are before the main dependency            $dependencyClasses = array_merge($dependencyClasses, $subDependencies, [$dependencyClass,]);        }        return $dependencyClasses;    }    // can be overridden for add external arguments    public function getTemplateArgs(Settings $settings): array    {        // using static for child support        $resourceInfo = self::getResourceInfoForTwigTemplate($settings, static::class);        $pathToTemplate = $resourceInfo ?            $resourceInfo[self::RESOURCE_KEY_RELATIVE_RESOURCE_PATH] . $settings->getTwigExtension() :            '';        $namespace      = $resourceInfo[self::RESOURCE_KEY_NAMESPACE] ?? '';        $templateArgs = [            self::TEMPLATE_KEY_NAMESPACE => $namespace,            self::TEMPLATE_KEY_TEMPLATE  => $pathToTemplate,            self::TEMPLATE_KEY_IS_LOADED => $this->isLoaded,        ];        if (! $pathToTemplate) {            $settings->callErrorCallback(                [                    'error' => 'Twig template is missing for the block',                    // using static for child support                    'class' => static::class,                ]            );        }        foreach ($this->fieldsInfo as $fieldName => $fieldType) {            $value = $this->{$fieldName};            if ($value instanceof self) {                $value = $value->getTemplateArgs($settings);            }            $templateArgs[$fieldName] = $value;        }        return $templateArgs;    }    protected function getFieldType(string $fieldName): ?string    {        $fieldType = null;        try {            // used static for child support            $property = new ReflectionProperty(static::class, $fieldName);        } catch (Exception $ex) {            return $fieldType;        }        if (! $property->isProtected()) {            return $fieldType;        }        return $property->getType() ?            $property->getType()->getName() :            '';    }    private function readFieldsInfo(): void    {        $fieldNames = array_keys(get_class_vars(static::class));        foreach ($fieldNames as $fieldName) {            $fieldType = $this->getFieldType($fieldName);            // only protected fields            if (is_null($fieldType)) {                continue;            }            $this->fieldsInfo[$fieldName] = $fieldType;        }    }    private function autoInitFields(): void    {        foreach ($this->fieldsInfo as $fieldName => $fieldType) {            // ignore fields without a type            if (! $fieldType) {                continue;            }            $defaultValue = null;            switch ($fieldType) {                case 'int':                case 'float':                    $defaultValue = 0;                    break;                case 'bool':                    $defaultValue = false;                    break;                case 'string':                    $defaultValue = '';                    break;                case 'array':                    $defaultValue = [];                    break;            }            try {                if (is_subclass_of($fieldType, Block::class)) {                    $defaultValue = new $fieldType();                }            } catch (Exception $ex) {                $defaultValue = null;            }            // ignore fields with a custom type (null by default)            if (is_null($defaultValue)) {                continue;            }            $this->{$fieldName} = $defaultValue;        }    }    final protected function load(): void    {        $this->isLoaded = true;    }}
BlockTest.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocks\Block;use LightSource\FrontBlocks\Settings;use org\bovigo\vfs\vfsStream;use UnitTester;class BlockTest extends Unit{    protected UnitTester $tester;    public function testReadProtectedFields()    {        $block = new class extends Block {            protected $loadedField;        };        $this->assertEquals(            ['loadedField',],            array_keys($block->getFieldsInfo())        );    }    public function testIgnoreReadPublicFields()    {        $block = new class extends Block {            public $ignoredField;        };        $this->assertEquals(            [],            array_keys($block->getFieldsInfo())        );    }    public function testReadFieldWithType()    {        $block = new class extends Block {            protected string $loadedField;        };        $this->assertEquals(            [                'loadedField' => 'string',            ],            $block->getFieldsInfo()        );    }    public function testReadFieldWithoutType()    {        $block = new class extends Block {            protected $loadedField;        };        $this->assertEquals(            [                'loadedField' => '',            ],            $block->getFieldsInfo()        );    }    public function testAutoInitIntField()    {        $block = new class extends Block {            protected int $int;            public function getInt()            {                return $this->int;            }        };        $this->assertTrue(0 === $block->getInt());    }    public function testAutoInitFloatField()    {        $block = new class extends Block {            protected float $float;            public function getFloat()            {                return $this->float;            }        };        $this->assertTrue(0.0 === $block->getFloat());    }    public function testAutoInitStringField()    {        $block = new class extends Block {            protected string $string;            public function getString()            {                return $this->string;            }        };        $this->assertTrue('' === $block->getString());    }    public function testAutoInitBoolField()    {        $block = new class extends Block {            protected bool $bool;            public function getBool()            {                return $this->bool;            }        };        $this->assertTrue(false === $block->getBool());    }    public function testAutoInitArrayField()    {        $block = new class extends Block {            protected array $array;            public function getArray()            {                return $this->array;            }        };        $this->assertTrue([] === $block->getArray());    }    public function testAutoInitBlockField()    {        $testBlock        = new class extends Block {        };        $testBlockClass   = get_class($testBlock);        $block            = new class ($testBlockClass) extends Block {            protected $block;            private $testClass;            public function __construct($testClass)            {                $this->testClass = $testClass;                parent::__construct();            }            public function getFieldType(string $fieldName): ?string            {                return ('block' === $fieldName ?                    $this->testClass :                    parent::getFieldType($fieldName));            }            public function getBlock()            {                return $this->block;            }        };        $actualBlockClass = $block->getBlock() ?            get_class($block->getBlock()) :            '';        $this->assertEquals($actualBlockClass, $testBlockClass);    }    public function testIgnoreAutoInitFieldWithoutType()    {        $block = new class extends Block {            protected $default;            public function getDefault()            {                return $this->default;            }        };        $this->assertTrue(null === $block->getDefault());    }    public function testGetResourceInfo()    {        $settings = new Settings();        $settings->addBlocksFolder('TestNamespace', 'test-folder');        $this->assertEquals(            [                Block::RESOURCE_KEY_NAMESPACE              => 'TestNamespace',                Block::RESOURCE_KEY_FOLDER                 => 'test-folder',                Block::RESOURCE_KEY_RELATIVE_RESOURCE_PATH => 'Button/Theme/Red/ButtonThemeRed',                Block::RESOURCE_KEY_RELATIVE_BLOCK_PATH    => 'Button/Theme/Red',                Block::RESOURCE_KEY_RESOURCE_NAME          => 'ButtonThemeRed',            ],            Block::getResourceInfo($settings, 'TestNamespace\\Button\\Theme\\Red\\ButtonThemeRed')        );    }    public function testGetDependenciesWithSubDependenciesRecursively()    {        $spanBlock   = new class extends Block {        };        $buttonBlock = new class ($spanBlock) extends Block {            protected $spanBlock;            public function __construct($spanBlock)            {                parent::__construct();                $this->spanBlock = $spanBlock;            }        };        $formBlock   = new class ($buttonBlock) extends Block {            protected $buttonBlock;            public function __construct($buttonBlock)            {                parent::__construct();                $this->buttonBlock = $buttonBlock;            }        };        $this->assertEquals(            [                get_class($spanBlock),                get_class($buttonBlock),            ],            $formBlock->getDependencies()        );    }    public function testGetDependenciesInRightOrder()    {        $spanBlock   = new class extends Block {        };        $buttonBlock = new class ($spanBlock) extends Block {            protected $spanBlock;            public function __construct($spanBlock)            {                parent::__construct();                $this->spanBlock = $spanBlock;            }        };        $formBlock   = new class ($buttonBlock) extends Block {            protected $buttonBlock;            public function __construct($buttonBlock)            {                parent::__construct();                $this->buttonBlock = $buttonBlock;            }        };        $this->assertEquals(            [                get_class($spanBlock),                get_class($buttonBlock),            ],            $formBlock->getDependencies()        );    }    public function testGetDependenciesWhenBlocksAreDependentFromEachOther()    {        $buttonBlock = new class extends Block {            protected $formBlock;            public function __construct()            {                parent::__construct();            }            public function setFormBlock($formBlock)            {                $this->formBlock = $formBlock;            }        };        $formBlock   = new class ($buttonBlock) extends Block {            protected $buttonBlock;            public function __construct($buttonBlock)            {                parent::__construct();                $this->buttonBlock = $buttonBlock;            }        };        $buttonBlock->setFormBlock($formBlock);        $this->assertEquals(            [                get_class($buttonBlock),            ],            $formBlock->getDependencies()        );    }    public function testGetDependenciesWithoutDuplicatesWhenSeveralWithOneType()    {        function getButtonBlock()        {            return new class extends Block {            };        }        $inputBlock = new class (getButtonBlock()) extends Block {            protected $buttonBlock;            public function __construct($buttonBlock)            {                parent::__construct();                $this->buttonBlock = $buttonBlock;            }        };        $formBlock = new class ($inputBlock) extends Block {            protected $inputBlock;            protected $firstButtonBlock;            protected $secondButtonBlock;            public function __construct($inputBlock)            {                parent::__construct();                $this->inputBlock        = $inputBlock;                $this->firstButtonBlock  = getButtonBlock();                $this->secondButtonBlock = getButtonBlock();            }        };        $this->assertEquals(            [                get_class(getButtonBlock()),                get_class($inputBlock),            ],            $formBlock->getDependencies()        );    }    public function testGetTemplateArgsWhenBlockContainsBuiltInTypes()    {        $settings    = new Settings();        $buttonBlock = new class extends Block {            protected string $name;            public function __construct()            {                parent::__construct();                $this->name = 'button';            }        };        $this->assertEquals(            [                Block::TEMPLATE_KEY_NAMESPACE => '',                Block::TEMPLATE_KEY_TEMPLATE  => '',                Block::TEMPLATE_KEY_IS_LOADED => false,                'name'                        => 'button',            ],            $buttonBlock->getTemplateArgs($settings)        );    }    public function testGetTemplateArgsWhenBlockContainsAnotherBlockRecursively()    {        $settings    = new Settings();        $spanBlock   = new class extends Block {            protected string $name;            public function __construct()            {                parent::__construct();                $this->name = 'span';            }        };        $buttonBlock = new class ($spanBlock) extends Block {            protected $spanBlock;            public function __construct($spanBlock)            {                parent::__construct();                $this->spanBlock = $spanBlock;            }        };        $formBlock   = new class ($buttonBlock) extends Block {            protected $buttonBlock;            public function __construct($buttonBlock)            {                parent::__construct();                $this->buttonBlock = $buttonBlock;            }        };        $this->assertEquals(            [                Block::TEMPLATE_KEY_NAMESPACE => '',                Block::TEMPLATE_KEY_TEMPLATE  => '',                Block::TEMPLATE_KEY_IS_LOADED => false,                'buttonBlock'                 => [                    Block::TEMPLATE_KEY_NAMESPACE => '',                    Block::TEMPLATE_KEY_TEMPLATE  => '',                    Block::TEMPLATE_KEY_IS_LOADED => false,                    'spanBlock'                   => [                        Block::TEMPLATE_KEY_NAMESPACE => '',                        Block::TEMPLATE_KEY_TEMPLATE  => '',                        Block::TEMPLATE_KEY_IS_LOADED => false,                        'name'                        => 'span',                    ],                ],            ],            $formBlock->getTemplateArgs($settings)        );    }    public function testGetTemplateArgsWhenTemplateIsInParent()    {        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());        $blocksFolder  = vfsStream::create(            [                'ButtonBase'  => [                    'ButtonBase.php'  => $this->tester->getBlockClassFile(                        $namespace . '\ButtonBase',                        'ButtonBase',                        '\\' . Block::class                    ),                    'ButtonBase.twig' => '',                ],                'ButtonChild' => [                    'ButtonChild.php' => $this->tester->getBlockClassFile(                        $namespace . '\ButtonChild',                        'ButtonChild',                        '\\' . $namespace . '\ButtonBase\ButtonBase'                    ),                ],            ],            $rootDirectory        );        $settings = new Settings();        $settings->addBlocksFolder($namespace, $blocksFolder->url());        $buttonChildClass = $namespace . '\ButtonChild\ButtonChild';        $buttonChild      = new $buttonChildClass();        if (! $buttonChild instanceof Block) {            $this->fail("Class doesn't child to Block");        }        $this->assertEquals(            [                Block::TEMPLATE_KEY_NAMESPACE => $namespace,                Block::TEMPLATE_KEY_TEMPLATE  => 'ButtonBase/ButtonBase.twig',                Block::TEMPLATE_KEY_IS_LOADED => false,            ],            $buttonChild->getTemplateArgs($settings)        );    }}

BlocksLoader

Предоставляет опциональную возможность автозагрузки всех блоков, при этом у каждого блока будет вызван статический метод ::onLoad, что может быть использовано для расширения, например поддержки ajax запросов и т.д.

BlocksLoader.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks;class BlocksLoader{    private array $loadedBlockClasses;    private Settings $settings;    public function __construct(Settings $settings)    {        $this->loadedBlockClasses = [];        $this->settings           = $settings;    }    final public function getLoadedBlockClasses(): array    {        return $this->loadedBlockClasses;    }    private function tryToLoadBlock(string $phpClass): bool    {        $isLoaded = false;        if (            ! class_exists($phpClass, true) ||            ! is_subclass_of($phpClass, Block::class)        ) {            // without any error, because php files can contain other things            return $isLoaded;        }        call_user_func([$phpClass, 'onLoad']);        return true;    }    private function loadBlocks(string $namespace, array $phpFileNames): void    {        foreach ($phpFileNames as $phpFileName) {            $phpClass = implode('\\', [$namespace, str_replace('.php', '', $phpFileName),]);            if (! $this->tryToLoadBlock($phpClass)) {                continue;            }            $this->loadedBlockClasses[] = $phpClass;        }    }    private function loadDirectory(string $directory, string $namespace): void    {        // exclude ., ..        $fs = array_diff(scandir($directory), ['.', '..']);        $phpFilePreg = '/.php$/';        $phpFileNames      = Helper::arrayFilter(            $fs,            function ($f) use ($phpFilePreg) {                return (1 === preg_match($phpFilePreg, $f));            },            false        );        $subDirectoryNames = Helper::arrayFilter(            $fs,            function ($f) {                return false === strpos($f, '.');            },            false        );        foreach ($subDirectoryNames as $subDirectoryName) {            $subDirectory = implode(DIRECTORY_SEPARATOR, [$directory, $subDirectoryName]);            $subNamespace = implode('\\', [$namespace, $subDirectoryName]);            $this->loadDirectory($subDirectory, $subNamespace);        }        $this->loadBlocks($namespace, $phpFileNames);    }    final public function loadAllBlocks(): void    {        $blockFoldersInfo = $this->settings->getBlockFoldersInfo();        foreach ($blockFoldersInfo as $namespace => $folder) {            $this->loadDirectory($folder, $namespace);        }    }}
BlocksLoaderTest.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocks\Block;use LightSource\FrontBlocks\BlocksLoader;use LightSource\FrontBlocks\Settings;use org\bovigo\vfs\vfsStream;use UnitTester;class BlocksLoaderTest extends Unit{    protected UnitTester $tester;    public function testLoadAllBlocksWhichChildToBlock()    {        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());        $blocksFolder  = vfsStream::create(            [                'ButtonBase'  => [                    'ButtonBase.php' => $this->tester->getBlockClassFile(                        $namespace . '\ButtonBase',                        'ButtonBase',                        '\\' . Block::class                    ),                ],                'ButtonChild' => [                    'ButtonChild.php' => $this->tester->getBlockClassFile(                        $namespace . '\ButtonChild',                        'ButtonChild',                        '\\' . $namespace . '\ButtonBase\ButtonBase'                    ),                ],            ],            $rootDirectory        );        $settings = new Settings();        $settings->addBlocksFolder($namespace, $blocksFolder->url());        $blocksLoader = new BlocksLoader($settings);        $blocksLoader->loadAllBlocks();        $this->assertEquals(            [                $namespace . '\ButtonBase\ButtonBase',                $namespace . '\ButtonChild\ButtonChild',            ],            $blocksLoader->getLoadedBlockClasses()        );    }    public function testLoadAllBlocksIgnoreNonChild()    {        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());        $blocksFolder  = vfsStream::create(            [                'ButtonBase' => [                    'ButtonBase.php' => '<?php use ' . $namespace . '; class ButtonBase{}',                ],            ],            $rootDirectory        );        $settings = new Settings();        $settings->addBlocksFolder($namespace, $blocksFolder->url());        $blocksLoader = new BlocksLoader($settings);        $blocksLoader->loadAllBlocks();        $this->assertEmpty($blocksLoader->getLoadedBlockClasses());    }    public function testLoadAllBlocksInSeveralFolders()    {        $rootDirectory   = $this->tester->getUniqueDirectory(__METHOD__);        $firstFolderUrl  = $rootDirectory->url() . '/First';        $secondFolderUrl = $rootDirectory->url() . '/Second';        $firstNamespace  = $this->tester->getUniqueControllerNamespaceWithAutoloader(            __METHOD__ . '_first',            $firstFolderUrl,        );        $secondNamespace = $this->tester->getUniqueControllerNamespaceWithAutoloader(            __METHOD__ . '_second',            $secondFolderUrl,        );        vfsStream::create(            [                'First'  => [                    'ButtonBase' => [                        'ButtonBase.php' => $this->tester->getBlockClassFile(                            $firstNamespace . '\ButtonBase',                            'ButtonBase',                            '\\' . Block::class                        ),                    ],                ],                'Second' => [                    'ButtonBase' => [                        'ButtonBase.php' => $this->tester->getBlockClassFile(                            $secondNamespace . '\ButtonBase',                            'ButtonBase',                            '\\' . Block::class                        ),                    ],                ],            ],            $rootDirectory        );        $settings = new Settings();        $settings->addBlocksFolder($firstNamespace, $firstFolderUrl);        $settings->addBlocksFolder($secondNamespace, $secondFolderUrl);        $blocksLoader = new BlocksLoader($settings);        $blocksLoader->loadAllBlocks();        $this->assertEquals(            [                $firstNamespace . '\ButtonBase\ButtonBase',                $secondNamespace . '\ButtonBase\ButtonBase',            ],            $blocksLoader->getLoadedBlockClasses()        );    }}

Renderer

Связующий класс, объединяет вспомогательные классы, предоставляет функцию рендера блока, содержит список использованных блоков и их ресурсы (css, js)

Renderer.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks;class Renderer{    private Settings $settings;    private TwigWrapper $twigWrapper;    private BlocksLoader $blocksLoader;    private array $usedBlockClasses;    public function __construct(Settings $settings)    {        $this->settings         = $settings;        $this->twigWrapper             = new TwigWrapper($settings);        $this->blocksLoader     = new BlocksLoader($settings);        $this->usedBlockClasses = [];    }    final public function getSettings(): Settings    {        return $this->settings;    }    final public function getTwigWrapper(): TwigWrapper    {        return $this->twigWrapper;    }    final public function getBlocksLoader(): BlocksLoader    {        return $this->blocksLoader;    }    final public function getUsedBlockClasses(): array    {        return $this->usedBlockClasses;    }    final public function getUsedResources(string $extension, bool $isIncludeSource = false): string    {        $resourcesContent = '';        foreach ($this->usedBlockClasses as $usedBlockClass) {            $getResourcesInfoCallback = [$usedBlockClass, 'getResourceInfo'];            if (! is_callable($getResourcesInfoCallback)) {                $this->settings->callErrorCallback(                    [                        'message' => "Block class doesn't exist",                        'class'   => $usedBlockClass,                    ]                );                continue;            }            $resourceInfo = call_user_func_array(                $getResourcesInfoCallback,                [                    $this->settings,                ]            );            $pathToResourceFile = $resourceInfo['folder'] .                                  DIRECTORY_SEPARATOR . $resourceInfo['relativeResourcePath'] . $extension;            if (! is_file($pathToResourceFile)) {                continue;            }            $resourcesContent .= $isIncludeSource ?                "\n/* " . $resourceInfo['resourceName'] . " */\n" :                '';            $resourcesContent .= file_get_contents($pathToResourceFile);        }        return $resourcesContent;    }    final public function render(Block $block, array $args = [], bool $isPrint = false): string    {        $dependencies           = array_merge($block->getDependencies(), [get_class($block),]);        $newDependencies        = array_diff($dependencies, $this->usedBlockClasses);        $this->usedBlockClasses = array_merge($this->usedBlockClasses, $newDependencies);        $templateArgs           = $block->getTemplateArgs($this->settings);        $templateArgs           = Helper::arrayMergeRecursive($templateArgs, $args);        $namespace              = $templateArgs[Block::TEMPLATE_KEY_NAMESPACE];        $relativePathToTemplate = $templateArgs[Block::TEMPLATE_KEY_TEMPLATE];        // log already exists        if (! $relativePathToTemplate) {            return '';        }        return $this->twigWrapper->render($namespace, $relativePathToTemplate, $templateArgs, $isPrint);    }}
RendererTest.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocks\Block;use LightSource\FrontBlocks\Renderer;use LightSource\FrontBlocks\Settings;use org\bovigo\vfs\vfsStream;use UnitTester;class RendererTest extends Unit{    protected UnitTester $tester;    public function testRenderAddsBlockToUsedList()    {        $settings = new Settings();        $renderer = new Renderer($settings);        $button = new class extends Block {        };        $renderer->render($button);        $this->assertEquals(            [                get_class($button),            ],            $renderer->getUsedBlockClasses()        );    }    public function testRenderAddsBlockDependenciesToUsedList()    {        $settings = new Settings();        $renderer = new Renderer($settings);        $button = new class extends Block {        };        $form   = new class ($button) extends Block {            protected $button;            public function __construct($button)            {                parent::__construct();                $this->button = $button;            }        };        $renderer->render($form);        $this->assertEquals(            [                get_class($button),                get_class($form),            ],            $renderer->getUsedBlockClasses()        );    }    public function testRenderAddsDependenciesBeforeBlockToUsedList()    {        $settings = new Settings();        $renderer = new Renderer($settings);        $button = new class extends Block {        };        $form   = new class ($button) extends Block {            protected $button;            public function __construct($button)            {                parent::__construct();                $this->button = $button;            }        };        $renderer->render($form);        $this->assertEquals(            [                get_class($button),                get_class($form),            ],            $renderer->getUsedBlockClasses()        );    }    public function testRenderAddsBlockToUsedListOnce()    {        $settings = new Settings();        $renderer = new Renderer($settings);        $button = new class extends Block {        };        $renderer->render($button);        $renderer->render($button);        $this->assertEquals(            [                get_class($button),            ],            $renderer->getUsedBlockClasses()        );    }    public function testRenderAddsBlockDependenciesToUsedListOnce()    {        $settings = new Settings();        $renderer = new Renderer($settings);        $button = new class extends Block {        };        $form   = new class ($button) extends Block {            protected $button;            public function __construct($button)            {                parent::__construct();                $this->button = $button;            }        };        $footer = new class ($button) extends Block {            protected $button;            public function __construct($button)            {                parent::__construct();                $this->button = $button;            }        };        $renderer->render($form);        $renderer->render($footer);        $this->assertEquals(            [                get_class($button),                get_class($form),                get_class($footer),            ],            $renderer->getUsedBlockClasses()        );    }    public function testGetUsedResources()    {        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());        $blocksFolder  = vfsStream::create(            [                'Button' => [                    'Button.php' => $this->tester->getBlockClassFile(                        $namespace . '\Button',                        'Button',                        '\\' . Block::class                    ),                    'Button.css' => '.button{}',                ],                'Form'   => [                    'Form.php' => $this->tester->getBlockClassFile(                        $namespace . '\Form',                        'Form',                        '\\' . Block::class                    ),                    'Form.css' => '.form{}',                ],            ],            $rootDirectory        );        $formClass   = $namespace . '\Form\Form';        $form        = new $formClass();        $buttonClass = $namespace . '\Button\Button';        $button      = new $buttonClass();        $settings = new Settings();        $settings->addBlocksFolder($namespace, $blocksFolder->url());        $renderer = new Renderer($settings);        $renderer->render($button);        $renderer->render($form);        $this->assertEquals('.button{}.form{}', $renderer->getUsedResources('.css'));    }    public function testGetUsedResourcesWithIncludedSource()    {        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());        $blocksFolder  = vfsStream::create(            [                'Button' => [                    'Button.php' => $this->tester->getBlockClassFile(                        $namespace . '\Button',                        'Button',                        '\\' . Block::class                    ),                    'Button.css' => '.button{}',                ],                'Form'   => [                    'Form.php' => $this->tester->getBlockClassFile(                        $namespace . '\Form',                        'Form',                        '\\' . Block::class                    ),                    'Form.css' => '.form{}',                ],            ],            $rootDirectory        );        $formClass   = $namespace . '\Form\Form';        $form        = new $formClass();        $buttonClass = $namespace . '\Button\Button';        $button      = new $buttonClass();        $settings = new Settings();        $settings->addBlocksFolder($namespace, $blocksFolder->url());        $renderer = new Renderer($settings);        $renderer->render($button);        $renderer->render($form);        $this->assertEquals(            "\n/* Button */\n.button{}\n/* Form */\n.form{}",            $renderer->getUsedResources('.css', true)        );    }}

Settings

Вспомогательный класс, основные данные это пути к блокам и их пространства имен

Settings.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks;class Settings{    private array $blockFoldersInfo;    private array $twigArgs;    private string $twigExtension;    private $errorCallback;    public function __construct()    {        $this->blockFoldersInfo = [];        $this->twigArgs         = [            // will generate exception if a var doesn't exist instead of replace to NULL            'strict_variables' => true,            // disable autoescape to prevent break data            'autoescape'       => false,        ];        $this->twigExtension    = '.twig';        $this->errorCallback    = null;    }    public function addBlocksFolder(string $namespace, string $folder): void    {        $this->blockFoldersInfo[$namespace] = $folder;    }    public function setTwigArgs(array $twigArgs): void    {        $this->twigArgs = array_merge($this->twigArgs, $twigArgs);    }    public function setErrorCallback(?callable $errorCallback): void    {        $this->errorCallback = $errorCallback;    }    public function setTwigExtension(string $twigExtension): void    {        $this->twigExtension = $twigExtension;    }    public function getBlockFoldersInfo(): array    {        return $this->blockFoldersInfo;    }    public function getBlockFolderInfoByBlockClass(string $blockClass): ?array    {        foreach ($this->blockFoldersInfo as $blockNamespace => $blockFolder) {            if (0 !== strpos($blockClass, $blockNamespace)) {                continue;            }            return [                'namespace' => $blockNamespace,                'folder'    => $blockFolder,            ];        }        return null;    }    public function getTwigArgs(): array    {        return $this->twigArgs;    }    public function getTwigExtension(): string    {        return $this->twigExtension;    }    public function callErrorCallback(array $errors): void    {        if (! is_callable($this->errorCallback)) {            return;        }        call_user_func_array($this->errorCallback, [$errors,]);    }}
SettingsTest.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocks\Settings;class SettingsTest extends Unit{    public function testGetBlockFolderInfoByBlockClass()    {        $settings = new Settings();        $settings->addBlocksFolder('TestNamespace', 'test-folder');        $this->assertEquals(            [                'namespace' => 'TestNamespace',                'folder'    => 'test-folder',            ],            $settings->getBlockFolderInfoByBlockClass('TestNamespace\Class')        );    }    public function testGetBlockFolderInfoByBlockClassWhenSeveral()    {        $settings = new Settings();        $settings->addBlocksFolder('FirstNamespace', 'first-namespace');        $settings->addBlocksFolder('SecondNamespace', 'second-namespace');        $this->assertEquals(            [                'namespace' => 'FirstNamespace',                'folder'    => 'first-namespace',            ],            $settings->getBlockFolderInfoByBlockClass('FirstNamespace\Class')        );    }    public function testGetBlockFolderInfoByBlockClassIgnoreWrong()    {        $settings = new Settings();        $settings->addBlocksFolder('TestNamespace', 'test-folder');        $this->assertEquals(            null,            $settings->getBlockFolderInfoByBlockClass('WrongNamespace\Class')        );    }}

TwigWrapper

Класс обертка для Twig пакета, обеспечиват работу с шаблонами. Также расширили twig своей функцией _include (которая является оберткой для встроенного include и использует наши поля _isLoaded и _template из метода Block->getTemplateArgs выше) и фильтром _merge (который отличается тем, что рекурсивно сливает массивы).

TwigWrapper.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks;use Exception;use Twig\Environment;use Twig\Loader\FilesystemLoader;use Twig\Loader\LoaderInterface;use Twig\TwigFilter;use Twig\TwigFunction;class TwigWrapper{    private ?LoaderInterface $twigLoader;    private ?Environment $twigEnvironment;    private Settings $settings;    public function __construct(Settings $settings, ?LoaderInterface $twigLoader = null)    {        $this->twigEnvironment = null;        $this->settings        = $settings;        $this->twigLoader      = $twigLoader;        $this->init();    }    private static function GetTwigNamespace(string $namespace)    {        return str_replace('\\', '_', $namespace);    }    // e.g for extend a twig with adding a new filter    public function getEnvironment(): ?Environment    {        return $this->twigEnvironment;    }    private function extendTwig(): void    {        $this->twigEnvironment->addFilter(            new TwigFilter(                '_merge',                function ($source, $additional) {                    return Helper::arrayMergeRecursive($source, $additional);                }            )        );        $this->twigEnvironment->addFunction(            new TwigFunction(                '_include',                function ($block, $args = []) {                    $block = Helper::arrayMergeRecursive($block, $args);                    return $block[Block::TEMPLATE_KEY_IS_LOADED] ?                        $this->render(                            $block[Block::TEMPLATE_KEY_NAMESPACE],                            $block[Block::TEMPLATE_KEY_TEMPLATE],                            $block                        ) :                        '';                }            )        );    }    private function init(): void    {        $blockFoldersInfo = $this->settings->getBlockFoldersInfo();        try {            // can be already init (in tests)            if (! $this->twigLoader) {                $this->twigLoader = new FilesystemLoader();                foreach ($blockFoldersInfo as $namespace => $folder) {                    $this->twigLoader->addPath($folder, self::GetTwigNamespace($namespace));                }            }            $this->twigEnvironment = new Environment($this->twigLoader, $this->settings->getTwigArgs());        } catch (Exception $ex) {            $this->twigEnvironment = null;            $this->settings->callErrorCallback(                [                    'message' => $ex->getMessage(),                    'file'    => $ex->getFile(),                    'line'    => $ex->getLine(),                    'trace'   => $ex->getTraceAsString(),                ]            );            return;        }        $this->extendTwig();    }    public function render(string $namespace, string $template, array $args = [], bool $isPrint = false): string    {        $html = '';        // twig isn't loaded        if (is_null($this->twigEnvironment)) {            return $html;        }        // can be empty, e.g. for tests        $twigNamespace = $namespace ?            '@' . self::GetTwigNamespace($namespace) . '/' :            '';        try {            // will generate ean exception if a template doesn't exist OR broken            // also if a var doesn't exist (if using a 'strict_variables' flag, see Twig_Environment->__construct)            $html .= $this->twigEnvironment->render($twigNamespace . $template, $args);        } catch (Exception $ex) {            $html = '';            $this->settings->callErrorCallback(                [                    'message'  => $ex->getMessage(),                    'file'     => $ex->getFile(),                    'line'     => $ex->getLine(),                    'trace'    => $ex->getTraceAsString(),                    'template' => $template,                ]            );        }        if ($isPrint) {            echo $html;        }        return $html;    }}
TwigWrapperTest.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocks\Block;use LightSource\FrontBlocks\Settings;use LightSource\FrontBlocks\TwigWrapper;use Twig\Loader\ArrayLoader;class TwigWrapperTest extends Unit{    private function renderBlock(array $blocks, string $template, array $renderArgs = []): string    {        $twigLoader = new ArrayLoader($blocks);        $settings   = new Settings();        $twig       = new TwigWrapper($settings, $twigLoader);        return $twig->render('', $template, $renderArgs);    }    public function testExtendTwigIncludeFunctionWhenBlockIsLoaded()    {        $blocks     = [            'form.twig'   => '{{ _include(button) }}',            'button.twig' => 'button content',        ];        $template   = 'form.twig';        $renderArgs = [            'button' => [                Block::TEMPLATE_KEY_NAMESPACE => '',                Block::TEMPLATE_KEY_TEMPLATE  => 'button.twig',                Block::TEMPLATE_KEY_IS_LOADED => true,            ],        ];        $this->assertEquals('button content', $this->renderBlock($blocks, $template, $renderArgs));    }    public function testExtendTwigIncludeFunctionWhenBlockNotLoaded()    {        $blocks     = [            'form.twig'   => '{{ _include(button) }}',            'button.twig' => 'button content',        ];        $template   = 'form.twig';        $renderArgs = [            'button' => [                Block::TEMPLATE_KEY_NAMESPACE => '',                Block::TEMPLATE_KEY_TEMPLATE  => 'button.twig',                Block::TEMPLATE_KEY_IS_LOADED => false,            ],        ];        $this->assertEquals('', $this->renderBlock($blocks, $template, $renderArgs));    }    public function testExtendTwigIncludeFunctionWhenArgsPassed()    {        $blocks     = [            'form.twig'   => '{{ _include(button,{classes:["test-class",],}) }}',            'button.twig' => '{{ classes|join(" ") }}',        ];        $template   = 'form.twig';        $renderArgs = [            'button' => [                Block::TEMPLATE_KEY_NAMESPACE => '',                Block::TEMPLATE_KEY_TEMPLATE  => 'button.twig',                Block::TEMPLATE_KEY_IS_LOADED => true,                'classes'                     => ['own-class',],            ],        ];        $this->assertEquals('own-class test-class', $this->renderBlock($blocks, $template, $renderArgs));    }    public function testExtendTwigMergeFilter()    {        $blocks     = [            'button.twig' => '{{ {"array":["first",],}|_merge({"array":["second",],}).array|join(" ") }}',        ];        $template   = 'button.twig';        $renderArgs = [];        $this->assertEquals('first second', $this->renderBlock($blocks, $template, $renderArgs));    }}

Helper

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

Helper.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks;abstract class Helper{    final public static function arrayFilter(array $array, callable $callback, bool $isSaveKeys): array    {        $arrayResult = array_filter($array, $callback);        return $isSaveKeys ?            $arrayResult :            array_values($arrayResult);    }    final public static function arrayMergeRecursive(array $args1, array $args2): array    {        foreach ($args2 as $key => $value) {            if (intval($key) === $key) {                $args1[] = $value;                continue;            }            // recursive sub-merge for internal arrays            if (                is_array($value) &&                key_exists($key, $args1) &&                is_array($args1[$key])            ) {                $value = self::arrayMergeRecursive($args1[$key], $value);            }            $args1[$key] = $value;        }        return $args1;    }}
HelperTest.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocks\Helper;class HelperTest extends Unit{    public function testArrayFilterWithoutSaveKeys()    {        $this->assertEquals(            [                0 => '2',            ],            Helper::ArrayFilter(                ['1', '2'],                function ($value) {                    return '1' !== $value;                },                false            )        );    }    public function testArrayFilterWithSaveKeys()    {        $this->assertEquals(            [                1 => '2',            ],            Helper::ArrayFilter(                ['1', '2'],                function ($value) {                    return '1' !== $value;                },                true            )        );    }    public function testArrayMergeRecursive()    {        $this->assertEquals(            [                'classes' => [                    'first',                    'second',                ],                'value'   => 2,            ],            Helper::arrayMergeRecursive(                [                    'classes' => [                        'first',                    ],                    'value'   => 1,                ],                [                    'classes' => [                        'second',                    ],                    'value'   => 2,                ]            )        );    }}

Это был последний класс, теперь можно переходить к демонстрационному примеру.

Демонстрационный пример

Ниже приведу демонстрационный пример использования пакета, с одним чистым css для наглядности, если кому-то интересен пример с scss/webpack смотрите ссылки в конце статьи.

Создаем блоки для теста, пусть это будут Header, Article и Button. Header и Button будут независимыми блоками, Article будет содержкать Button.

Header

Header.php
<?phpnamespace LightSource\FrontBlocksSample\Header;use LightSource\FrontBlocks\Block;class Header extends Block{    protected string $name;    public function loadByTest()    {        parent::load();        $this->name = 'I\'m Header';    }}
Header.twig
<div class="header">    {{ name }}</div>
Header.css
.header {    color: green;    border:1px solid green;    padding: 10px;}

Button

Button.php
<?phpnamespace LightSource\FrontBlocksSample\Button;use LightSource\FrontBlocks\Block;class Button extends Block{    protected string $name;    public function loadByTest()    {        parent::load();        $this->name = 'I\'m Button';    }}
Button.twig
<div class="button">    {{ name }}</div>
Button.css
.button {    color: black;    border: 1px solid black;    padding: 10px;}

Article

Article.php
<?phpnamespace LightSource\FrontBlocksSample\Article;use LightSource\FrontBlocks\Block;use LightSource\FrontBlocksSample\Button\Button;class Article extends Block{    protected string $name;    protected Button $button;    public function loadByTest()    {        parent::load();        $this->name = 'I\'m Article, I contain another block';        $this->button->loadByTest();    }}
Article.twig
<div class="article">    <p class="article__name">{{ name }}</p>    {{ _include(button) }}</div>
Article.css
.article {    color: orange;    border: 1px solid orange;    padding: 10px;}.article__name {    margin: 0 0 10px;    line-height: 1.5;}

Далее подключаем пакет и рендерим блоки, результат вставляем в html код страницы, в шапке выводим использованный css код

example.php
<?phpuse LightSource\FrontBlocks\{    Renderer,    Settings};use LightSource\FrontBlocksSample\{    Article\Article,    Header\Header};require_once __DIR__ . '/vendors/vendor/autoload.php';//// settingsini_set('display_errors', 1);$settings = new Settings();$settings->addBlocksFolder('LightSource\FrontBlocksSample', __DIR__ . '/Blocks');$settings->setErrorCallback(    function (array $errors) {        // todo log or any other actions        echo '<pre>' . print_r($errors, true) . '</pre>';    });$renderer = new Renderer($settings);//// usage$header = new Header();$header->loadByTest();$article = new Article();$article->loadByTest();$content = $renderer->render($header);$content .= $renderer->render($article);$css     = $renderer->getUsedResources('.css', true);//// html?><html><head>    <title>Example</title>    <style>        <?= $css ?>    </style>    <style>        .article {            margin-top: 10px;        }    </style></head><body><?= $content ?></body></html>

в результате вывод будет таким

example.png

Послесловие

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

Понравилась статья? Не забудь проголосовать.

Ссылки:

репозиторий с данным пакетом

репозиторий с демонстрационным примером

репозиторий с примером использования scss и js в блоках (webpack сборщик)

репозиторий с примером использования в WordPress теме (здесь вы также можете увидеть пример расширения класса блока и использования автозагрузки, что добавляет поддержку ajax запросов для блоков)

P.S. Благодарю @alexmixaylov, @bombe и @rpsv за конструктивные комментарии к первой части.

Подробнее..

JAM-стэк нищета на стероидах

31.10.2020 16:04:39 | Автор: admin
Создавая сайты для малого бизнеса я сталкиваюсь с двумя крайностями. Но только я, как программист. Пользователи не сталкиваются, ведь нельзя столкнуться с тем чего для тебя не существует. Первая крайность это когда клиент покупает за 50$ в месяц очередной хостинг для Wordpress. Человек не знает, что для Wordpress не нужен специальный хостинг, что такой специальный хостинг как правило хуже чем обычный хостинг и содержит кучу ограничений и стоит дороже. Вторая крайность это когда используется JAM-стэк ради экономии. Но это экономия в плохом смысле этого слова, когда вы экономите на спичках, используя генератор для питания паяльника, от которого вы прикуриваете.

Говоря простым языком, JAM-стэк это куча костылей, использование которых выйдет боком всем, и прежде всего разработчику. Говоря техническим языком, JAM-стэк это система интегрированных костылей для создания статических сайтов, с использованием SAAS для персистенции данных, а также значительной долей рендеринга на клиенте. Как делали статические сайты деды во времена своей молодости? Они писали простые HTML и CSS файлы и выкладывали их на хостинг по FTP. Как делали статические сайты наши отцы во времена своей буйной молодости? Они использовали Jekyll/Octopress, или любой из сотни генераторов статических сайтов, а полученные HTML и CSS файлы заливали на github pages через коммит, цепляли нужный домен. Некоторые тогда еще устраивали игрища с Disqus, потому что никак кроме игрищ я это назвать не могу, ведь пользователь с учёткой Disqus для оставления комментариев на вашем сайте исчезающе редок.

По балансу цена/затраты времени/сложность поддержки/ограничения в разработке это все было хорошим вариантом. Когда это переставало быть хорошим вариантом, покупался хостинг с php за пару баксов в месяц. Статические страницы ошаблонивались и обзаводились солидным функционалом полноценного сайта. И все было хорошо, а Енисей был из светлого пива. Но наши великие предки нашли нормальную работу и больше не страдают такой фигней. Теперь ей страдаем мы, и что же нам, молодым, смешливым, которым все легко, может предложить индустрия? Она гордо кровохаркает нам в лицо JAM-стэком, и говорит: Не дождетесь!.

JAM-стэк это новейший подход к созданию статических сайтов, и Gatsby.JS как один из пророков его. Gatsby это самый яркий представитель жанра, возводящий насмешку над идеей статических сайтов в абсолют, переводя ее таким образом уже в разряд постиронии. Начнем с того, что Gatsby построен поверх React. Того самого React, который создавался для сайтов, где нужен компонентный подход, т.е. есть какие-то пользовательские интерфейсы, т.е. есть манипуляция с данными. Но у нас ведь статический сайт? Нет? Ретрограда ответ! Теперь это не проблема, у нас ведь есть сервисы типа Netlify и Contentful. Они предоставляют вам возможность через API делать AJAX запросы на их сервера и получать или записывать контент. Т.е. обычная база данных доступная через тридесятую задницу. Зато бесплатно. Первые N запросов, или пользователей, плюс лимит на размер блобов. Акция: уложись во все ограничения и получи оплату от заказчика* (*количество попыток ограничено).

Почему же на первый взгляд это выглядит привлекательно для бизнеса? Потому что React у всех на слуху, а Reactо-макак, которые еще вчера смогли войти в АйТи и готовых работать за копейки очень много. Для Reactо-макак это привлекательно потому что хоть какой-то способ поднять денег и набить портфолио. А сидя у мамки на шее можно литералли не платить ни за хостинг, ни за базу. По этой же причине сумневающемуся заказчику можно, увидев результат, прикинуть нужно ли оно ему вообще, и перестать отвечать на сообщения горе-фрилансера. Также заказчика и исполнителя объединяет достаточно малая компетентность, где первый не понимает как вообще все это работает, а второй не понимает, что сайты можно делать и по-другому.

В итоге, за редким исключением, о котором позже, проигрывают все. React и его производные это сложный инструмент с большой экосистемой и огромными проблемами, которые часто под силу только программистам на React, а не Reactо-макакам. 10 лет назад существовал популярный цирковой номер под названием вытащить меню со всеми вложенными подменю за один SQL запрос. Теперь у нас есть его идейный наследник вытащить все данные из нужного сервиса через один GraphQL запрос. Gatsby тянет за собой больше 500 зависимостей, и зная скорость обновления JS экосистемы можно смело сказать через полгода что-то сломается, если вам понадобится новый сторонний виджет. Через 2 года вы будете заниматься черрипикингом версий, чтобы просто пересобрать это чудо в новый релиз. Да шучу я, шучу! Оно может и в первый раз не собраться по инструкциям с сайта. Если роскомнадзор в очередном порыве заботы о гражданах заблокирует ваш serverless database server, или просто сменит тариф, то развлекаться со всем этим опять вам. Кстати в отличие от традиционных статических сайтов билд сайта на Gatsby !== исходникам сайта. Так что стратегия бэкапа и развертывания этого чуда включая базу данных, да даже без неё, весьма занятная. Но самая мякотка начнется, если уродец созданный школьниками на кривых технологиях понадобится развивать. Поверьте, у PHP верхний предел ублюдочности legacy-кода гораздо ниже, что бы там про него не рассказывали!

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

Так что же тогда подходящий случай для использования JAM-стэка в его современном виде? На мой взгляд, это ситуация когда ваш достаточно адекватный знакомый или родственник просит вас, React-программиста имеющего нормальную высокооплачиваему работу по своему профилю, сделать относительно простой сайт в свободное время. И вы используя существующие навыки сможете это быстро сделать, объяснив при этом человеку все недостатки такого подхода. И если он согласен, то тогда вперед. Иначе просто расскажите ему про Wordpress и wp2static.

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

Перевод Автоматизируем установку WordPress с NGINX Unit и Ubuntu

23.10.2020 08:18:45 | Автор: admin


Есть множество материалов по установке WordPress, поиск в Google по ключевым словам "WordPress install" выдаст порядка полумиллиона результатов. Но тем не менее фактически среди них весьма мало годных руководств, по которым можно установить и настроить WordPress и нижележащую операционную систему так, чтобы они были способны к поддержке в течение длительного периода времени. Возможно, правильные настройки сильно зависят от конкретных потребностей, или же это связано с тем, что подробное объяснение делает статью тяжелой для чтения.


В этой статье мы постараемся собрать лучшее из двух подходов, предоставляя скрипт на bash для автоматической установки WordPress на Ubuntu, а также пройдемся по нему, поясняя, что делает каждый его кусочек, а также на какие компромиссы мы пошли при его разработке. Если вы опытный пользователь можете пропустить текст статьи и просто взять скрипт для модификации и использования в ваших окружениях. На выходе скрипта получается настраиваемая установка Wordpress с поддержкой Lets Encrypt, работающая на NGINX Unit и пригодная для промышленного применения.


Разработанная архитектура для развертывания WordPress с использованием NGINX Unit описана в более старой статье, сейчас мы также дополнительно настроим вещи, которые там не были охвачены (как и во многих других руководствах):


  • WordPress CLI
  • Let's Encrypt и сертификаты TLS\SSL
  • Автоматическое обновление сертификатов
  • Кэширование NGINX
  • Сжатие NGINX
  • Поддержка HTTPS и HTTP/2
  • Автоматизация процесса

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


Требования


  • Сервер-контейнер (LXC или LXD), виртуальная машина, или обычный железный сервер, с не менее чем 512Мб оперативной памяти и установленной Ubuntu 18.04 или более свежей.
  • Доступные из интернета порты 80 и 443
  • Доменное имя, связанное с публичным ip-адресом этого сервера
  • Доступ с правами root (sudo).

Обзор архитектуры


Архитектура такая же, как было описано ранее, трехуровневое web-приложение. Оно состоит из скриптов PHP, исполняемых на обработчике PHP, и статических файлов, обрабатываемых веб-сервером.



Общие принципы


  • Многие команды для настройки в скрипте обернуты в условия (if) для идемпотентности: скрипт можно запускать несколько раз без риска изменения настроек, которые уже готовы.
  • Скрипт старается устанавливать ПО из репозиториев, так что вы можете применять обновления системы в одну команду (apt upgrade для Ubuntu).
  • Команды стараются определить, что они запускаются в контейнере, чтобы изменить соответствующим образом свои настройки.
  • Для того, чтобы задать число запускаемых процессов\потоков в настройках, скрипт пробует угадать автоматические параметры настройки для работы в контейнерах, виртуальных машинах, железных серверах.
  • При описании настроек всегда думаем в первую очередь об автоматизации, которая, как мы надеемся, станет основой для создания вашей собственной инфраструктуры как кода.
  • Все команды запускаются от пользователя root, потому что они изменяют основные системные настройки, но непосредственно WordPress работает от обычного пользователя.

Установка переменных окружения


Установите следующие переменные окружения, прежде чем запускать скрипт:


  • WORDPRESS_DB_PASSWORD пароль к базе данных WordPress
  • WORDPRESS_ADMIN_USER имя администратора WordPress
  • WORDPRESS_ADMIN_PASSWORD пароль администратора WordPress
  • WORDPRESS_ADMIN_EMAIL email администратора WordPress
  • WORDPRESS_URL полный URL сайта WordPress, начиная с https://.
  • LETS_ENCRYPT_STAGING пустая по-умолчанию, но, выставив значение в 1, вы будете использовать staging сервера Lets Encrypt, необходимые для частого запроса сертификатов при тестировании ваших настроек, иначе Lets Encrypt может временно заблокировать ваш ip-адрес из-за большого числа запросов.

Скрипт проверяет, что эти связанные с WordPress переменные выставлены, и завершает работу, если нет.
Строки скрипта 572-576 проверяют значение LETS_ENCRYPT_STAGING.


Установка производных переменных окружения


Скрипт в строках 55-61 выставляет следующие переменные окружения, либо в некоторое жестко заданное значение, либо с применением значения, полученного из переменных, установленных в предыдущем разделе:


  • DEBIAN_FRONTEND="noninteractive" сообщает приложениям, что они запускаются в скрипте и нет возможности взаимодействия с пользователем.
  • WORDPRESS_CLI_VERSION="2.4.0" версия приложения WordPress CLI.
  • WORDPRESS_CLI_MD5= "dedd5a662b80cda66e9e25d44c23b25c" контрольная сумма исполняемого файла WordPress CLI 2.4.0 (версия указывается в переменной WORDPRESS_CLI_VERSION). Скрипт на 162 строке использует это значение для проверки, что был скачан корректный файл WordPress CLI.
  • UPLOAD_MAX_FILESIZE="16M" максимальный размер файла, который может быть закачан в WordPress. Эта настройка используется в нескольких местах, так что проще задавать ее в одном месте.
  • TLS_HOSTNAME= "$(echo ${WORDPRESS_URL} | cut -d'/' -f3)" hostname системы, извлекаемый из переменной WORDPRESS_URL. Используется для получения соответствующих TLS/SSL сертификатов от Lets Encrypt, а также для внутренней проверки WordPress.
  • NGINX_CONF_DIR="/etc/nginx" путь к каталогу с настройками NGINX, включая основной файл nginx.conf.
  • CERT_DIR="/etc/letsencrypt/live/${TLS_HOSTNAME}" путь к сертификатам Lets Encrypt для сайта WordPress, получаемый из переменной TLS_HOSTNAME.

Назначение hostname WordPress серверу


Скрипт устанавливает hostname серверу, чтобы значение соответствовало доменному имени сайта. Это не обязательно, но так удобнее отправлять исходящую почту через SMTP при настройке единственного сервера, как это настраивается скриптом.


код скрипта
# Change the hostname to be the same as the WordPress hostnameif [ ! "$(hostname)" == "${TLS_HOSTNAME}" ]; then  echo " Changing hostname to ${TLS_HOSTNAME}"  hostnamectl set-hostname "${TLS_HOSTNAME}"fi

Добавление hostname в /etc/hosts


Дополнение WPCron используется для запуска периодических задач, требует, чтобы WordPress мог получить доступ к самому себе через HTTP. Чтобы убедиться, что WP-Cron работает корректно на всех окружениях, скрипт добавляет строчку в файл /etc/hosts, так что WordPress может получить доступ к самому себе через интерфейс loopback:


код скрипта
# Add the hostname to /etc/hostsif [ "$(grep -m1 "${TLS_HOSTNAME}" /etc/hosts)" = "" ]; then  echo " Adding hostname ${TLS_HOSTNAME} to /etc/hosts so that WordPress can ping itself"  printf "::1 %s\n127.0.0.1 %s\n" "${TLS_HOSTNAME}" "${TLS_HOSTNAME}" >> /etc/hostsfi

Установка инструментов, требуемых для последующих шагов


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


код скрипта
# Make sure tools needed for install are presentecho " Installing prerequisite tools"apt-get -qq updateapt-get -qq install -y \  bc \  ca-certificates \  coreutils \  curl \  gnupg2 \  lsb-release

Добавление репозиториев NGINX Unit и NGINX


Скрипт устанавливает NGINX Unit и NGINX с открытым исходным кодом из официальных репозиториев NGINX, чтобы удостовериться, что используются версии с последними обновлениями безопасности и исправлениями ошибок.


Скрипт добавляет репозиторий NGINX Unit, а затем репозиторий NGINX, добавляя ключ репозиториев и файлы настроек apt, задающих доступ к репозиториям через интернет.


Реальная установка NGINX Unit и NGINX происходит в следующем разделе. Мы предварительно добавляем репозитории, чтобы не обновлять метаданные несколько раз, что делает установку быстрее.


код скрипта
# Install the NGINX Unit repositoryif [ ! -f /etc/apt/sources.list.d/unit.list ]; then  echo " Installing NGINX Unit repository"  curl -fsSL https://nginx.org/keys/nginx_signing.key | apt-key add -  echo "deb https://packages.nginx.org/unit/ubuntu/ $(lsb_release -cs) unit" > /etc/apt/sources.list.d/unit.listfi# Install the NGINX repositoryif [ ! -f /etc/apt/sources.list.d/nginx.list ]; then  echo " Installing NGINX repository"  curl -fsSL https://nginx.org/keys/nginx_signing.key | apt-key add -  echo "deb https://nginx.org/packages/mainline/ubuntu $(lsb_release -cs) nginx" > /etc/apt/sources.list.d/nginx.listfi

Установка NGINX, NGINX Unit, PHP MariaDB, Certbot (Lets Encrypt) и их зависимостей


Как только все репозитории добавлены, обновляем метаданные и устанавливаем приложения. Пакеты, устанавливаемые скриптом, также включают расширения PHP, рекомендуемые при запуске WordPress.org


код скрипта
echo " Updating repository metadata"apt-get -qq update# Install PHP with dependencies and NGINX Unitecho " Installing PHP, NGINX Unit, NGINX, Certbot, and MariaDB"apt-get -qq install -y --no-install-recommends \  certbot \  python3-certbot-nginx \  php-cli \  php-common \  php-bcmath \  php-curl \  php-gd \  php-imagick \  php-mbstring \  php-mysql \  php-opcache \  php-xml \  php-zip \  ghostscript \  nginx \  unit \  unit-php \  mariadb-server

Настройка PHP для использования с NGINX Unit и WordPress


Скрипт создает файл настроек в каталоге conf.d. Тут задается максимальный размер загружаемых файлов для PHP, включается вывод ошибок PHP в STDERR, так что они будут записаны в журнал NGINX Unit, а также перезапускается NGINX Unit.


код скрипта
# Find the major and minor PHP version so that we can write to its conf.d directoryPHP_MAJOR_MINOR_VERSION="$(php -v | head -n1 | cut -d' ' -f2 | cut -d'.' -f1,2)"if [ ! -f "/etc/php/${PHP_MAJOR_MINOR_VERSION}/embed/conf.d/30-wordpress-overrides.ini" ]; then  echo " Configuring PHP for use with NGINX Unit and WordPress"  # Add PHP configuration overrides  cat > "/etc/php/${PHP_MAJOR_MINOR_VERSION}/embed/conf.d/30-wordpress-overrides.ini" << EOM; Set a larger maximum upload size so that WordPress can handle; bigger media files.upload_max_filesize=${UPLOAD_MAX_FILESIZE}post_max_size=${UPLOAD_MAX_FILESIZE}; Write error log to STDERR so that error messages show up in the NGINX Unit logerror_log=/dev/stderrEOMfi# Restart NGINX Unit because we have reconfigured PHPecho " Restarting NGINX Unit"service unit restart

Задание настроек базы данных MariaDB для WordPress


Мы выбрали MariaDB вместо MySQL, поскольку у нее больше активность сообщества, кроме того, она, возможно, предоставляет более высокую производительность по умолчанию (вероятно, тут все проще: чтобы поставить MySQL, надо добавить еще один репозиторий, прим. переводчика).


Скрипт создает новую базу данных и создает учетные данные для доступа WordPress через интерфейс loopback:


код скрипта
# Set up the WordPress databaseecho " Configuring MariaDB for WordPress"mysqladmin create wordpress || echo "Ignoring above error because database may already exist"mysql -e "GRANT ALL PRIVILEGES ON wordpress.* TO \"wordpress\"@\"localhost\" IDENTIFIED BY \"$WORDPRESS_DB_PASSWORD\"; FLUSH PRIVILEGES;"

Установка программы WordPress CLI


На этом шаге скрипт устанавливает программу WP-CLI. С его помощью можно установить и управлять настройками WordPress без необходимости ручной правки файлов, обновления базы или входа в панель управления. Также с его помощью можно установить темы и дополнения и выполнить обновление WordPress.


код скрипта
if [ ! -f /usr/local/bin/wp ]; then  # Install the WordPress CLI  echo " Installing the WordPress CLI tool"  curl --retry 6 -Ls "https://github.com/wp-cli/wp-cli/releases/download/v${WORDPRESS_CLI_VERSION}/wp-cli-${WORDPRESS_CLI_VERSION}.phar" > /usr/local/bin/wp  echo "$WORDPRESS_CLI_MD5 /usr/local/bin/wp" | md5sum -c -  chmod +x /usr/local/bin/wpfi

Установка и настройка WordPress


Скрипт устанавливает последнюю версию WordPress в каталог /var/www/wordpress, а также изменяет настройки:


  • Соединение с базой данных работает через unix domain socket вместо TCP на loopback, чтобы сократить трафик TCP.
  • WordPress добавляет префикс https:// к URL, если клиенты соединяются с NGINX по протоколу HTTPS, а также отправляет удаленный hostname (как это предоставляет NGINX) в PHP. Мы применяем кусочек кода, чтобы это настроить.
  • WordPress нужно HTTPS для входа
  • Структура URL по молчанию основывается на ресурсах
  • Выставляются правильные права на файловой системе для каталога WordPress.

код скрипта
if [ ! -d /var/www/wordpress ]; then  # Create WordPress directories  mkdir -p /var/www/wordpress  chown -R www-data:www-data /var/www  # Download WordPress using the WordPress CLI  echo " Installing WordPress"  su -s /bin/sh -c 'wp --path=/var/www/wordpress core download' www-data  WP_CONFIG_CREATE_CMD="wp --path=/var/www/wordpress config create --extra-php --dbname=wordpress --dbuser=wordpress --dbhost=\"localhost:/var/run/mysqld/mysqld.sock\" --dbpass=\"${WORDPRESS_DB_PASSWORD}\""  # This snippet is injected into the wp-config.php file when it is created;  # it informs WordPress that we are behind a reverse proxy and as such  # allows it to generate links using HTTPS  cat > /tmp/wp_forwarded_for.php << 'EOM'/* Turn HTTPS 'on' if HTTP_X_FORWARDED_PROTO matches 'https' */if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false) {    $_SERVER['HTTPS'] = 'on';}if (isset($_SERVER['HTTP_X_FORWARDED_HOST'])) {    $_SERVER['HTTP_HOST'] = $_SERVER['HTTP_X_FORWARDED_HOST'];}EOM  # Create WordPress configuration  su -s /bin/sh -p -c "cat /tmp/wp_forwarded_for.php | ${WP_CONFIG_CREATE_CMD}" www-data  rm /tmp/wp_forwarded_for.php  su -s /bin/sh -p -c "wp --path=/var/www/wordpress config set 'FORCE_SSL_ADMIN' 'true'" www-data  # Install WordPress  WP_SITE_INSTALL_CMD="wp --path=/var/www/wordpress core install --url=\"${WORDPRESS_URL}\" --title=\"${WORDPRESS_SITE_TITLE}\" --admin_user=\"${WORDPRESS_ADMIN_USER}\" --admin_password=\"${WORDPRESS_ADMIN_PASSWORD}\" --admin_email=\"${WORDPRESS_ADMIN_EMAIL}\" --skip-email"  su -s /bin/sh -p -c "${WP_SITE_INSTALL_CMD}" www-data  # Set permalink structure to a sensible default that isn't in the UI  su -s /bin/sh -p -c "wp --path=/var/www/wordpress option update permalink_structure '/%year%/%monthnum%/%postname%/'" www-data  # Remove sample file because it is cruft and could be a security problem  rm /var/www/wordpress/wp-config-sample.php  # Ensure that WordPress permissions are correct  find /var/www/wordpress -type d -exec chmod g+s {} \;  chmod g+w /var/www/wordpress/wp-content  chmod -R g+w /var/www/wordpress/wp-content/themes  chmod -R g+w /var/www/wordpress/wp-content/pluginsfi

Настройка NGINX Unit


Скрипт настраивает NGINX Unit для запуска PHP и обработки путей WordPress, изолируя пространство имен процессов PHP и оптимизируя настройки производительности. Тут есть три функции, на которые стоит обратить внимание:


  • Поддержка пространств имен определяется по условию, основана на проверке запуска скрипта в контейнере. Это нужно, поскольку большинство настроек контейнеров не поддерживают вложенный запуск контейнеров.
  • Если есть поддержка пространств имен, отключается пространство имен network. Это нужно, чтобы позволить WordPress одновременно подключаться и к endpoints и быть доступным в интернете.
  • Максимальное число процессов определяется следующим образом: (Доступная память для запущенных MariaDB и NGINX Uniy)/(предел по оперативной памяти в PHP + 5)
    Это значение устанавливается в настройках NGINX Unit.

Также это значение подразумевает, что всегда есть как минимум два запущенных процесса PHP, что важно, поскольку WordPress делает много асинхронных запросов к самому себе, а без дополнительных процессов запуск, к примеру, WP-Cron, сломается. Вы, возможно, захотите увеличить или уменьшить эти ограничения, основываясь на ваших локальных настройках, потому что созданные настройки здесь консервативные. На большинстве производственных систем настройки находятся между 10 и 100.


код скрипта
if [ "${container:-unknown}" != "lxc" ] && [ "$(grep -m1 -a container=lxc /proc/1/environ | tr -d '\0')" == "" ]; then  NAMESPACES='"namespaces": {        "cgroup": true,        "credential": true,        "mount": true,        "network": false,        "pid": true,        "uname": true    }'else  NAMESPACES='"namespaces": {}'fiPHP_MEM_LIMIT="$(grep 'memory_limit' /etc/php/7.4/embed/php.ini | tr -d ' ' | cut -f2 -d= | numfmt --from=iec)"AVAIL_MEM="$(grep MemAvailable /proc/meminfo | tr -d ' kB' | cut -f2 -d: | numfmt --from-unit=K)"MAX_PHP_PROCESSES="$(echo "${AVAIL_MEM}/${PHP_MEM_LIMIT}+5" | bc)"echo " Calculated the maximum number of PHP processes as ${MAX_PHP_PROCESSES}. You may want to tune this value due to variations in your configuration. It is not unusual to see values between 10-100 in production configurations."echo " Configuring NGINX Unit to use PHP and WordPress"cat > /tmp/wordpress.json << EOM{  "settings": {    "http": {      "header_read_timeout": 30,      "body_read_timeout": 30,      "send_timeout": 30,      "idle_timeout": 180,      "max_body_size": $(numfmt --from=iec ${UPLOAD_MAX_FILESIZE})    }  },  "listeners": {    "127.0.0.1:8080": {      "pass": "routes/wordpress"    }  },  "routes": {    "wordpress": [      {        "match": {          "uri": [            "*.php",            "*.php/*",            "/wp-admin/"          ]        },        "action": {          "pass": "applications/wordpress/direct"        }      },      {        "action": {          "share": "/var/www/wordpress",          "fallback": {            "pass": "applications/wordpress/index"          }        }      }    ]  },  "applications": {    "wordpress": {      "type": "php",      "user": "www-data",      "group": "www-data",      "processes": {        "max": ${MAX_PHP_PROCESSES},        "spare": 1      },      "isolation": {        ${NAMESPACES}      },      "targets": {        "direct": {          "root": "/var/www/wordpress/"        },        "index": {          "root": "/var/www/wordpress/",          "script": "index.php"        }      }    }  }}EOMcurl -X PUT --data-binary @/tmp/wordpress.json --unix-socket /run/control.unit.sock http://localhost/config

Настройка NGINX


Настройка основных параметров NGINX


Скрипт создает каталог для кэша NGINX, а затем создает основной файл настройки nginx.conf. Обратите внимание на число процессов-обработчиков и задание максимального размера файла для загрузки. Также есть строка, на которой подключается файл настройки сжатия, определяемый в следующем разделе, далее идут настройки кэширования.


код скрипта
# Make directory for NGINX cachemkdir -p /var/cache/nginx/proxyecho " Configuring NGINX"cat > ${NGINX_CONF_DIR}/nginx.conf << EOMuser nginx;worker_processes auto;error_log  /var/log/nginx/error.log warn;pid        /var/run/nginx.pid;events {    worker_connections  1024;}http {    include       ${NGINX_CONF_DIR}/mime.types;    default_type  application/octet-stream;    log_format  main  '\$remote_addr - \$remote_user [\$time_local] "\$request" '                      '\$status \$body_bytes_sent "\$http_referer" '                      '"\$http_user_agent" "\$http_x_forwarded_for"';    access_log  /var/log/nginx/access.log  main;    sendfile        on;    client_max_body_size ${UPLOAD_MAX_FILESIZE};    keepalive_timeout  65;    # gzip settings    include ${NGINX_CONF_DIR}/gzip_compression.conf;    # Cache settings    proxy_cache_path /var/cache/nginx/proxy        levels=1:2        keys_zone=wp_cache:10m        max_size=10g        inactive=60m        use_temp_path=off;    include ${NGINX_CONF_DIR}/conf.d/*.conf;}EOM

Настройка сжатия NGINX


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


код скрипта
cat > ${NGINX_CONF_DIR}/gzip_compression.conf << 'EOM'# Credit: https://github.com/h5bp/server-configs-nginx/# ----------------------------------------------------------------------# | Compression                                                        |# ----------------------------------------------------------------------# https://nginx.org/en/docs/http/ngx_http_gzip_module.html# Enable gzip compression.# Default: offgzip on;# Compression level (1-9).# 5 is a perfect compromise between size and CPU usage, offering about 75%# reduction for most ASCII files (almost identical to level 9).# Default: 1gzip_comp_level 6;# Don't compress anything that's already small and unlikely to shrink much if at# all (the default is 20 bytes, which is bad as that usually leads to larger# files after gzipping).# Default: 20gzip_min_length 256;# Compress data even for clients that are connecting to us via proxies,# identified by the "Via" header (required for CloudFront).# Default: offgzip_proxied any;# Tell proxies to cache both the gzipped and regular version of a resource# whenever the client's Accept-Encoding capabilities header varies;# Avoids the issue where a non-gzip capable client (which is extremely rare# today) would display gibberish if their proxy gave them the gzipped version.# Default: offgzip_vary on;# Compress all output labeled with one of the following MIME-types.# `text/html` is always compressed by gzip module.# Default: text/htmlgzip_types  application/atom+xml  application/geo+json  application/javascript  application/x-javascript  application/json  application/ld+json  application/manifest+json  application/rdf+xml  application/rss+xml  application/vnd.ms-fontobject  application/wasm  application/x-web-app-manifest+json  application/xhtml+xml  application/xml  font/eot  font/otf  font/ttf  image/bmp  image/svg+xml  text/cache-manifest  text/calendar  text/css  text/javascript  text/markdown  text/plain  text/xml  text/vcard  text/vnd.rim.location.xloc  text/vtt  text/x-component  text/x-cross-domain-policy;EOM

Настройка NGINX для WordPress


Далее скрипт создает файл настройки для WordPress default.conf в каталоге conf.d. Здесь настраивается:


  • Активация сертификатов TLS, полученных от Let's Encrypt через Certbot (его настройка будет в следующем разделе)
  • Настройка параметров безопасности TLS, основанная на рекомендациях от Let's Encrypt
  • Подключение кэширования пропускаемых запросов на 1 час по умолчанию
  • Отключение журналирования доступа, а также журналирования ошибок, если файл не найден, для двух общих запрашиваемых файлов: favicon.ico и robots.txt
  • Запрет доступа к скрытым файлам и некоторым файлам .php, чтобы предотвратить нелегальный доступ или непреднамеренный запуск
  • Отключение журналирования доступа для статики и файлов шрифтов
  • Задание заголовка Access-Control-Allow-Origin для файлов шрифтов
  • Добавление маршрутизации для index.php и прочей статики.

код скрипта
cat > ${NGINX_CONF_DIR}/conf.d/default.conf << EOMupstream unit_php_upstream {    server 127.0.0.1:8080;    keepalive 32;}server {    listen 80;    listen [::]:80;    # ACME-challenge used by Certbot for Let's Encrypt    location ^~ /.well-known/acme-challenge/ {      root /var/www/certbot;    }    location / {      return 301 https://${TLS_HOSTNAME}\$request_uri;    }}server {    listen      443 ssl http2;    listen [::]:443 ssl http2;    server_name ${TLS_HOSTNAME};    root        /var/www/wordpress/;    # Let's Encrypt configuration    ssl_certificate         ${CERT_DIR}/fullchain.pem;    ssl_certificate_key     ${CERT_DIR}/privkey.pem;    ssl_trusted_certificate ${CERT_DIR}/chain.pem;    include ${NGINX_CONF_DIR}/options-ssl-nginx.conf;    ssl_dhparam ${NGINX_CONF_DIR}/ssl-dhparams.pem;    # OCSP stapling    ssl_stapling on;    ssl_stapling_verify on;    # Proxy caching    proxy_cache wp_cache;    proxy_cache_valid 200 302 1h;    proxy_cache_valid 404 1m;    proxy_cache_revalidate on;    proxy_cache_background_update on;    proxy_cache_lock on;    proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;    location = /favicon.ico {        log_not_found off;        access_log off;    }    location = /robots.txt {        allow all;        log_not_found off;        access_log off;    }    # Deny all attempts to access hidden files such as .htaccess, .htpasswd,    # .DS_Store (Mac)    # Keep logging the requests to parse later (or to pass to firewall utilities    # such as fail2ban)    location ~ /\. {        deny all;    }    # Deny access to any files with a .php extension in the uploads directory;    # works in subdirectory installs and also in multi-site network.    # Keep logging the requests to parse later (or to pass to firewall utilities    # such as fail2ban).    location ~* /(?:uploads|files)/.*\.php\$ {        deny all;    }    # WordPress: deny access to wp-content, wp-includes PHP files    location ~* ^/(?:wp-content|wp-includes)/.*\.php\$ {        deny all;    }    # Deny public access to wp-config.php    location ~* wp-config.php {        deny all;    }    # Do not log access for static assets, media    location ~* \.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {        access_log off;    }    location ~* \.(?:svgz?|ttf|ttc|otf|eot|woff2?)$ {        add_header Access-Control-Allow-Origin "*";        access_log off;    }    location / {        try_files \$uri @index_php;    }    location @index_php {        proxy_socket_keepalive on;        proxy_http_version 1.1;        proxy_set_header Connection "";        proxy_set_header X-Real-IP \$remote_addr;        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;        proxy_set_header X-Forwarded-Proto \$scheme;        proxy_set_header Host \$host;        proxy_pass       http://unit_php_upstream;    }    location ~* \.php\$ {        proxy_socket_keepalive on;        proxy_http_version 1.1;        proxy_set_header Connection "";        proxy_set_header X-Real-IP \$remote_addr;        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;        proxy_set_header X-Forwarded-Proto \$scheme;        proxy_set_header Host \$host;        try_files        \$uri =404;        proxy_pass       http://unit_php_upstream;    }}EOM

Настройка Certbot для сертификатов от Let's Encrypt и их автоматическое продление


Certbot бесплатный инструмент от Electronic Frontier Foundation (EFF), с помощью которого можно получать и автоматически обновлять сертификаты TLS от Let's Encrypt. Скрипт выполняет следующие действия, приводящие к настройке Certbot для обработки сертификатов от Let's Encrypt в NGINX:


  • Останавливает NGINX
  • Скачивает рекомендуемые параметры TLS
  • Запускает Certbot, чтобы получить сертификаты для сайта
  • Перезапускает NGINX для использования сертификатов
  • Настраивает ежедневный запуск Certbot в 3:24 ночи для проверки необходимости обновления сертификатов, а также, при необходимости, скачивания новых сертификатов и перезагрузки NGINX.

код скрипта
echo " Stopping NGINX in order to set up Let's Encrypt"service nginx stopmkdir -p /var/www/certbotchown www-data:www-data /var/www/certbotchmod g+s /var/www/certbotif [ ! -f ${NGINX_CONF_DIR}/options-ssl-nginx.conf ]; then  echo " Downloading recommended TLS parameters"  curl --retry 6 -Ls -z "Tue, 14 Apr 2020 16:36:07 GMT" \    -o "${NGINX_CONF_DIR}/options-ssl-nginx.conf" \    "https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf" \    || echo "Couldn't download latest options-ssl-nginx.conf"fiif [ ! -f ${NGINX_CONF_DIR}/ssl-dhparams.pem ]; then  echo " Downloading recommended TLS DH parameters"  curl --retry 6 -Ls -z "Tue, 14 Apr 2020 16:49:18 GMT" \    -o "${NGINX_CONF_DIR}/ssl-dhparams.pem" \    "https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem" \    || echo "Couldn't download latest ssl-dhparams.pem"fi# If tls_certs_init.sh hasn't been run before, remove the self-signed certsif [ ! -d "/etc/letsencrypt/accounts" ]; then  echo " Removing self-signed certificates"  rm -rf "${CERT_DIR}"fiif [ "" = "${LETS_ENCRYPT_STAGING:-}" ] || [ "0" = "${LETS_ENCRYPT_STAGING}" ]; then  CERTBOT_STAGING_FLAG=""else  CERTBOT_STAGING_FLAG="--staging"fiif [ ! -f "${CERT_DIR}/fullchain.pem" ]; then  echo " Generating certificates with Let's Encrypt"  certbot certonly --standalone \         -m "${WORDPRESS_ADMIN_EMAIL}" \         ${CERTBOT_STAGING_FLAG} \         --agree-tos --force-renewal --non-interactive \         -d "${TLS_HOSTNAME}"fiecho " Starting NGINX in order to use new configuration"service nginx start# Write crontab for periodic Let's Encrypt cert renewalif [ "$(crontab -l | grep -m1 'certbot renew')" == "" ]; then  echo " Adding certbot to crontab for automatic Let's Encrypt renewal"  (crontab -l 2>/dev/null; echo "24 3 * * * certbot renew --nginx --post-hook 'service nginx reload'") | crontab -fi

Дополнительная настройка вашего сайта


Мы выше рассказали о том, как наш скрипт настраивает NGINX и NGINX Unit для обслуживания готового к промышленной работе сайта с включенным TLS\SSL. Вы можете также, в зависимости от ваших нужд, добавить в будущем:


  • Поддержку Brotli, улучшенное сжатие на лету по HTTPS
  • ModSecurity с правилами для WordPress, чтобы предотвратить автоматические атаки на ваш сайт
  • Резервное копирование для WordPress, подходящее вам
  • Защиту с помощью AppArmor (на Ubuntu)
  • Postfix или msmtp, чтобы WordPress мог отправлять почту
  • Проверки вашего сайта, чтобы вы понимали, сколько трафика он может выдержать

Для еще более лучшей производительности сайта мы рекомендуем обновиться до NGINX Plus, наш коммерческий продукт корпоративного уровня, основанный на NGINX c открытым исходным кодом. Его подписчики получат динамически загружаемый модуль Brotli, а также (за дополнительную оплату) NGINX ModSecurity WAF. Мы также предлагаем NGINX App Protect, модуль WAF для NGINX Plus, основанный на технологии, ведущей в отрасли безопасности, от F5.


N.B. За поддержкой высоконагруженного сайта вы можете обратиться к специалистам Southbridge. Обеспечим быструю и надежную работу вашего сайта или сервиса под любой нагрузкой.
Подробнее..

Правильное автоматическое заполнение метатегов alt и title изображений для WordPress

28.07.2020 20:05:54 | Автор: admin


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

Проблематика


WordPress по умолчанию устанавливает название файла в поле Заголовок, которое соответствует атрибуту title, а поле Атрибут alt, которое соответствует атрибуту alt, оставляет пустым. Это вызывает дополнительные манипуляции при заполнении атрибутов у каждого изображения. При использовании стандартного загрузчика, параметры файла выглядят следующем образом:



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

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

А также, предупреждает, что оставлять атрибуты пустыми нежелательно и рекомендует для каждой картинки указывать уникальный title. То есть alt и title не должны быть одинаковыми.

Google, в своей справке, упоминает только alt:
Замещающий текст в атрибутах alt делает контент доступным для пользователей, которые не видят изображения на страницах (например, потому что используют программы для чтения с экрана или из-за медленного подключения к Интернету).

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

Исходя из моего опыта, можно сделать вывод о том, что alt существенно важнее title. Но тем не менее заполнять все-таки лучше два атрибута. Помимо этого, важно учитывать, что название файла изображения на сайте (используя транслитерацию) должно соответствовать alt. То есть грамотная оптимизация изображения для поисковых систем может выглядеть следующем образом:

  • название файла: Метатеги для изображений.png;
  • адрес до изображения: /metategi-dlya-izobrazhenij.png;
  • alt: Метатеги для изображений;
  • title: Изображение метатеги для изображений.

Решение


Так как заполнять два атрибута бывает несколько утомительно, а title просто дополняет alt. Мы, в своих проектах используем какое-то дополнительное слово или конструкцию для title. Важно, чтобы конструкция была универсальная и подходила для всех картинок на сайте. Поэтому, Я написал простое решение, которое изменяет работу стандартного загрузчика WordPress следующем образом:

  • атрибут alt (alt): Название файла;
  • заголовк (title): Изображение название файла.

Получая, такие параметры файла:



Установка решения


Для установки решения необходимо добавить следующий код в functions.php вашей темы:

# Automatically sets the image Title, Alt-Text, Caption & Description upon uploadadd_action('add_attachment', 'pami_set_image_meta_upon_upload');# Helper functionif (!function_exists('pami_image_meta_first')) {function pami_image_meta_first($my_image_title, $encoding = 'UTF-8') {$my_image_title = mb_ereg_replace('^[\ ]+', '', $my_image_title);$my_image_title = mb_strtoupper(mb_substr($my_image_title, 0, 1, $encoding), $encoding). mb_substr($my_image_title, 1, mb_strlen($my_image_title), $encoding);return $my_image_title;}}# Main functionfunction pami_set_image_meta_upon_upload($post_ID) {if (!wp_attachment_is_image($post_ID)) return;$my_image_title = get_post($post_ID)->post_title;// Sanitize the title: remove hyphens, underscores & extra spaces:$my_image_title = preg_replace('%\s*[-_\s]+\s*%', ' ', $my_image_title);// Sanitize the title: capitalize first letter of every word (other letters lower case):$my_image_title = str_replace('"', '', $my_image_title);$my_image_title = str_replace('', '', $my_image_title);$my_image_title = str_replace('', '', $my_image_title);$my_image_title = str_replace('', '', $my_image_title);$my_image_title = str_replace(':', '', $my_image_title);$my_image_title = str_replace('  ', ' ', $my_image_title);$my_image_title = str_replace('   ', ' ', $my_image_title);$my_image_title = pami_image_meta_first(mb_strtolower($my_image_title));// Set the image Alt-Textupdate_post_meta($post_ID, '_wp_attachment_image_alt', $my_image_title);$my_image_title = mb_strtolower($my_image_title);$my_image_meta = ['ID' => $post_ID,'post_title' => 'Изображение  ' . $my_image_title, // Set image Title to sanitized title]; // Set the image meta (e.g. Title, Excerpt, Content)wp_update_post($my_image_meta);}

Предложенное решение так же удаляет лишние символы (кавычки, дефисы, двойне пробелы и другие символы) из названия изображения. А конструкцию для title Изображение , можно легко изменить на любую другую (56 строчка, при просмотре в редакторе).

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

P.S. Для тех кто не хочет добавлять код самостоятельно, предлагаю просто установить плагин Prostudio Auto Meta Images из официального репозитория WordPress.
Подробнее..

7 лучших БЕСПЛАТНХ альтернатив cPanel (издание 2020 года)

13.09.2020 22:06:31 | Автор: admin
image
Многие наши новые пользователи удивляются, когда узнают, что cPanel не бесплатна. Фактически, cPanel стремится наказать небольшие компании и отдельных разработчиков больше всего, с планами с одной лицензией, начинающимися с 15 долларов в месяц. Это в два раза больше, чем платит большинство наших пользователей за свой сервер в месяц!

К примеру, компания AlexHost.com продает сервера с отличным конфигом 1.5 GB RAM / 1 Ядро / 10GB SSD диска 11.88 в год! Соответственно вполне логично, что продуктивнее купить у этих ребят сервер и поставить на него альтернативную панель управления, которая будет ничем не хуже чем платная cPanel.

image

К тому же, поддержка проконсультирует и поможет подобрать оптимальный вариант панели. Но все же лучше быть осведомленным и самостоятельно разбираться в данном вопросе.
Хорошей новостью является то, что есть несколько отличных, многофункциональных бесплатных альтернатив cPanel, из которых можно выбирать.
Именно по этой причине мы подготовили данный материал к ознакомлению.
В этой статье мы познакомим вас с нашими любимыми альтернативами cPanel. И мы дадим вам ссылки на учебные пособия, которые помогут вам начать управлять серверами Linux без лишних затрат.

Как мы выбрали наши альтернативы cPanel:

Все эти альтернативы cPanel либо абсолютно бесплатны, либо предлагают многофункциональную бесплатную версию. Поскольку большинство наших клиентов не являются компаниями корпоративного уровня (и эти люди, вероятно, могут позволить себе заплатить за решение), мы уверены, что любой из этих вариантов должен быть бесплатным для вас. Кроме того, большинство этих альтернатив cPanel имеют открытый исходный код, что означает, что вы можете просматривать и изучать базовый код. Вы даже можете вносить изменения, если вы хотите и можете.
В этом прелесть мира FOSS разработчики достаточно опытны и щедры, чтобы создавать невероятные альтернативы и предлагать их бесплатно, просто чтобы сделать сообщество лучше.

7 лучших бесплатных альтернатив cPanel на 2020 год
image
Webmin это самая многофункциональная альтернатива cPanel, и именно поэтому это наша рекомендация 1 на 2020 год.

По сути, вы можете делать все, что можете, с платой cPanel, но совершенно бесплатно. Благодаря встроенным модулям вы можете создавать резервные копии файлов конфигурации, устанавливать и настраивать веб-серверы Apache, отслеживать пропускную способность, настраивать fail2ban, устанавливать брандмауэр iptables, администрировать пользователей, настраивать задания cron, защищать соединения SSH и многое другое.

Эта панель управления может выглядеть не очень хорошо, но поэтому мы рекомендуем комбинировать Webmin с Authentic Theme. Эти обновленные темы стали проще для глаз и немного облегчили работу с Webmin.

Независимо от того, используете ли вы Ubuntu, Debian или CentOS, разработчики Webmin предложат вам пакет и процедуру установки. Если вы хотите углубиться в подробности, исходный код Webmin доступен на GitHub.

image
CentOS Web Panel если вы используете CentOS на своем виртуальном частном сервере (VPS) и не думаете, что Webmin вам подходит, то CentOS Web Panel может быть лучшей бесплатной альтернативой cPanel для ваших нужд.

Вы можете использовать его для развертывания и администрирования веб-серверов Apache, брандмауэров, баз данных MySQL, SSL-сертификатов, обратного прокси-сервера Nginx, электронной почты с самостоятельным размещением и многого другого. Вы также можете просто управлять пользователями, развертывать резервные копии и следить за состоянием своей системы через монитор служб.

Одной из уникальных функций CentOS Web Panel является ее автофиксатор, который сканирует важные файлы конфигурации и пытается автоматически исправить их в случае, если вы (или сама панель) допустили ошибку.

Для установки вам потребуется обновленная установка CentOS, работающий стек LAMP и не менее 1 ГБ оперативной памяти. К сожалению, код CentOS Web Panel не является полностью открытым исходным кодом, но это многофункциональная альтернатива cPanel, которую можно совершенно бесплатно использовать.

ПРИМЕЧАНИЕ. Веб-панель CentOS официально поддерживается только в CentOS если вы используете Debian / Ubuntu, вам придется воспользоваться одним из других вариантов.

image
Ajenti это комплексная панель управления, которая позиционирует себя как инструмент администратора для более цивилизованного возраста, предоставляя вам быстрый и безопасный способ управления удаленным Linux-модулем в любое время с помощью повседневных инструментов, таких как веб-терминал, текстовый редактор, файловый менеджер, и другие."

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

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

Основной проект с открытым исходным кодом и может быть найден на GitHub.

image
YunoHost позиционирует себя как серверная операционная система, целью которой является сделать хостинг доступным для всех.

Он не должен быть таким же всеобъемлющим, как cPanel или Webmin, поскольку его единственная цель установить для вас различные приложения, размещаемые самостоятельно. Здесь вы не найдете управление брандмауэром или обратные прокси-серверы, только несколько официально поддерживаемых приложений для установки различных программ, таких как Baikal, Nextcloud, WordPress, Zerobin и другие.

YunoHost отличная бесплатная альтернатива cPanel для начинающих пользователей, которые хотят быстро начать работу с некоторыми базовыми приложениями.
Вы можете управлять своим VPS через веб-интерфейс YunoHost или командную строку. YunoHost официально поддерживает Debian 8 и кодируется преимущественно на Python под лицензией GPL с открытым исходным кодом. Код доступен на GitHub.

image
Froxlor считает себя легкой альтернативой Webmin.

С их сайта: Разработанная опытными администраторами серверов, эта панель с открытым исходным кодом (GPL) упрощает управление платформой хостинга. Его функции включают установку Let Encrypt, настройку PHP, управление MySQL и многое другое.

У Froxlor есть доступные пакеты Debian и .tar.gz для производственных установок. Официально поддерживается только Debian, но, возможно, с небольшими усилиями можно установить его и на Ubuntu. Froxlor распространяется по лицензии GPL 2.0 с исходным кодом на GitHub.

image
ISPConfig давний конкурент в бесплатном альтернативном пространстве cPanel и может быть темной лошадкой в этом сравнении.

Они хвастаются на своем сайте около 40000 загрузок в месяц и это, безусловно, много. При таком использовании вы знаете, что это надежная бесплатная альтернатива cPanel, получившая широкую поддержку среди разработчиков с открытым исходным кодом. Вы можете использовать ISPConfig для настройки веб-серверов Apache2 / nginx, почтовых серверов, DNS, зеркалирования и многого другого, как если бы вы использовали Webmin или Ajenti.

Самая уникальная особенность ISPConfig это возможность управлять несколькими серверами с одной панели управления. Если вы будете работать с несколькими серверами и хотите, чтобы на всех из них работали единообразно, без необходимости устанавливать одну и ту же панель управления на каждом из них, ISPConfig может стать бесплатной альтернативой cPanel, которая может упростить вашу жизнь.

Вы можете загрузить файл .targ.gz самостоятельно или следовать учебному пособию Идеальный сервер, чтобы настроить Debian 8, Apache2, BIND, Dovecot и ISPConfig 3.

ISPConfig работает с Debian, Ubuntu и CentOS, что делает его гибким практически для любого приложения. Исходный код доступен через репозиторий GitLab организации под лицензией BSD с открытым исходным кодом.

image
VestaCP это красиво разработанное ядро панели управления, написанное на Bash, которое понравится пуристам Linux.

Встроенные функции включают развертывание iptables / fail2ban для безопасности, Nginx и / или Apache для веб-сервера, различные почтовые решения, решения для мониторинга, резервные копии и многое другое. Если вы предпочитаете работать через командную строку, а не через веб-интерфейс, вы можете сделать это и с Vesta.
Вы можете использовать VestaCP с CentOS, Debian и Ubuntu, и он лицензируется с GNU. Исходный код доступен на GitHub.

Примечание. Начиная с лета 2018 года, мы слышали об увеличении объема автоматических атак на серверы VestaCP на основе неизвестных уязвимостей. Распространенные решения включают защиту ваших SSH-соединений с помощью использования и применения ключей и полное отключение пользователя root. Мы не собираемся полностью исключать VestaCP из этого списка из-за его популярности, но, как думают, потенциальный пользователь должен знать об этом.

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

Перевод Актуален ли PHP в 2021 году?

28.01.2021 18:09:16 | Автор: admin

Фокус внимания давно переместился с PHP на JavaScript и Python. Тем не менее у него выходят новые версии, а тесты производительности говорят о неплохом прогрессе. Насколько актуален PHP сегодня? Под катом размышления разработчика, который продолжает отдавать ему предпочтение.

Краткая история PHP

PHP разработал Расмус Лердорф в 1994 году. Лердорф создал набор скриптов, которые отслеживали посещения его онлайн-резюме, и назвал их Personal Home Page Tools (инструменты личной домашней страницы). Со временем название превратилось в PHP Tools. Он пополнял этот набор новыми инструментами, а потом решил переписать их: добавил взаимодействие с базами данных и многое другое. Так набор превратился во фреймворк.

Дальше эти инструменты продолжали эволюционировать в еще более сложные примитивы, а после публикации кода в open source в 1995 году число пользователей стало расти заметно быстрее. Если вас интересуют подробности этой истории, вы можете найти их на официальном сайте PHP.

Последняя версия языка сейчас PHP 8.0.

А что не так с PHP?

Уже долгие годы этот язык мишень для критики. Люди справедливо упрекают его за проблемы в работе, особенно старые версии. Лердорф разрабатывал PHP как язык шаблонизации, а не как полнофункциональный язык программирования. Поэтому у него есть ряд недостатков, которые сильно усложняют поддержку и разработку крупных приложений.

Слабая типизация

Лично мне в этом языке не нравится слабая типизация, позволяющая сочетать разные типы и выполнять их неявные преобразования. Рассмотрим такой пример:

echo "1" + 3;echo 1 + "3";echo "1" + "3";

Результатом этих операций будет число 4, то есть в контексте оператора сложения язык преобразует числа в строке в целочисленные значения. В некоторых случаях это может оказаться желательным или позволит сэкономить пару строк кода, но чем больше становится проект, тем сложнее будет поддержка.

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

Отсутствие пространств имен

Поддержка пространств имен была добавлена в PHP в версии 5.3. В более старых проектах приходилось создавать собственные типы пространств имен, в основном при помощи добавления пространств имен к названиям классов и методов. Из-за этого приходилось использовать повсюду абсурдно длинные имена.

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

В более поздних версиях языка таких проблем нет.

Противоречивые функции стандартной библиотеки

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

Вот некоторые примеры противоречивости наименований в строковых методах из документации:

  • strpos(string $haystack, string $needle, int $offset = 0): int|false: находит позицию первого вхождения подстроки в строке;

  • strsplit(string $string, int $length = 1): array : преобразует строку в массив;

  • explode(string $separator, string $string, int $limit = PHP_INT_MAX): array: разделяет строку по граничной строке.

Три разные функции: одна с префиксом str, вторая с префиксом str, а третья без префикса. Аргумент $string является первым для str_split, но вторым для explode. Вы можете изучить все строковые методы в документации этому паттерну следует множество функций, то есть среди них нет особого единообразия.

Суперглобальные переменные

Это больше относится к личным предпочтениям я ненавижу использовать глобальные переменные, а следовательно, и суперглобальные. Когда находишь какие-то старые самодельные проекты, особенно высока вероятность встретиться с печально известными переменными типа $SERVER или $REQUEST. Не поймите меня неправильно: иногда они очень полезны и время от времени их нужно использовать. Однако для безопасного использования этих значений первым делом нужно инкапсулировать их в многократно используемые классы. Если этого не сделать, то взаимодействие со значениями или внесение изменений в крупный проект будет очень сложной задачей, ведь с этими значениями связано множество скрытых зависимостей.

И что хорошего в PHP?

Язык на многих произвел плохое впечатление, но за последние годы он сильно улучшился. Благодаря релизу PHP7 язык модернизировался, в его основе появилось множество приятных особенностей, повысились скорость работы и удобство пользования.

Type hints

Это один из моих любимых способов модернизации старого кода на PHP: использование необязательных type hints, выполняющих преобразование типов, а также обеспечивающих документацию кода. Рассмотрим простую функцию:

function isValueSomething($value) {}

Если добавить type hints, код станет таким:

function isValueSomething(string $value): bool {}

Просто увидев сигнатуру функции, мы понимаем, что она ожидает строковое значение и вернет булев-результат. Можно добавить, что здесь полезно было бы применить правильное наименование, однако эти type hints гарантируют, что значения будут именно указанных типов и предоставляют IDE возможность автодополнения и статического анализа с предупреждениями и другими полезными вещами.

С версии 7.4 PHP позволяет задавать и типизированные свойства для классов:

class Person {    public string $firstName;    public string $lastName;    public int $age;    public ?string $job;}

Это означает, что у объектов Person будут строковые имя и фамилия, возраст в integer и допускающая пустое значение строка для должности. Чем больше классов тем полезнее эта возможность.

Улучшения синтаксиса

В поздних версиях PHP появилось много синтаксических улучшений:

  • стрелочные функции: fn ($x, $y) => $x + $y;

  • оператор объединения с неопределенным значением: $value = $array['key'] ?? 'default value';

  • присваивание с неопределенным значением: return $cache['key'] ??= computeSomeValue('key');

  • расширение массивов: $first = ['a', 'b']; $second = ['c', 'd']; $final= [$first, $second];

  • именованные аргументы: array_fill(start_index: 0, num: 100, value: 50);

  • разделитель числовых литералов: 299_792_458

Помимо синтаксических он содержит возможности для комплексных улучшений.

Constructor promotion

Взгляните на класс Person:

class Person {    private string $firstName;    private string $lastName;     protected int $age;    public ?string $job;    public function __construct(        string $firstName,        string $lastName,        int $age,        ?string $job    ){        $this->firstName = $firstName;        $this->lastName = $lastName;        $this->age = $age;        $this->job = $job;    }}

Вместо этого избыточно многословного кода PHP 8 поддерживает возможность написания такого кода:

class Person {    public function __construct(        private string $firstName,        private string $lastName,        protected int $age,        public ?string $job    ){}}

Удобно, не так ли?

Nullsafe-оператор

Этот оператор существовал в некоторых других языках наподобие JavaScript, но PHP его не поддерживал. Взгляните на код, который я взял из документации PHP:

<?if (is_null($repository)) {    $result = null;} else {    $user = $repository->getUser(5);    if (is_null($user)) {        $result = null;    } else {        $result = $user->name;    }}

Именно так писали логику в старых версиях PHP для учета проверок на null. Новый nullsafe-оператор позволяет свести это к такому коду:

$result = $repository?->getUser(5)?->name;

Разве не великолепно?

Объединенные типы

Хоть для меня это и не самая любимая штука, она все равно ценна в случаях, когда уже есть несколько возможных типов без type hints. Объединения позволяют определять в качестве вариантов несколько типов значений. Благодаря объединениям становится работающим вот такой код:

function doSomething(int|string $value): bool|array {}

Обычно наличие нескольких возвращаемых типов сигнализирует о возможности улучшения кода, однако предыдущие версии PHP вообще не позволяли задавать в таких случаях типы, поэтому это все равно усовершенствование.

Производительность

У меня нет четких данных для сопоставления производительности с другими языками, но по сравнению с предыдущими версиями скорость работы кода PHP заметно выросла. В дополнение к рывку, сделанному PHP 7 по сравнению с PHP5.6, все последующие релизы вносили улучшения, и эта тенденция продолжается. Проведенные Phoronix бенчмарки демонстрируют, что последняя версия PHP8 в три с лишним раза быстрее PHP5.6. В посте по ссылке выше есть подробные тесты, так что их стоит изучить.

В дополнение к этим бенчмаркам Kinsta также провела реальные испытания с WordPress. Вот результат для WordPress 5.3.

Точные цифры замеров таковы:

  • Результаты бенчмарка WordPress 5.3 с PHP 5.6: 97,71 запросов/с

  • Результаты бенчмарка WordPress 5.3 с PHP 7.0: 256,81 запросов/с

  • Результаты бенчмарка WordPress 5.3 с PHP 7.1: 256,99 запросов/с

  • Результаты бенчмарка WordPress 5.3 с PHP 7.2: 273,07 запросов/с

  • Результаты бенчмарка WordPress 5.3 с PHP 7.3: 305,59 запросов/с

  • Результаты бенчмарка WordPress 5.3 с PHP 7.4: 313,42 запросов/с

Пока в эти бенчмарки не включен PHP 8, однако видно, что даже версия 7.4 способна обрабатывать в три раза больше запросов по сравнению с 5.6, и это серьезное улучшение.

Вывод

В целом PHP за последние годы значительно улучшили, и с ним стало удобно работать. Я профессионально пишу на Golang, PHP и Python, но больше всего опыта у меня в PHP, поэтому я могу быть пристрастен. Однако, по моему мнению, PHP нашел идеальный баланс между гибкостью и поддерживаемостью.

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

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


P. S. А еще хотел напомнить, что на нашей платформе каждую неделю проходят полтора-два десятка бесплатных мероприятий, связанных с IT и программированием. Не все они так же хардкорны, как доклады на конференциях Олега Бунина и JUG.ru, хотя попадаются темы и для продвинутых. Например, летом мы делали расшифровку доклада сообщества JUGNsk из новосибирской Точки кипения Project Panama: как сделать Java ближе к железу. И это был классный рассказ.

Подробнее..

Длинная история про то, как мы веб-разработчика на фрилансерских сайтах искали, но так и не нашли

06.06.2021 20:07:08 | Автор: admin

Понадобилось мне тут недавно фрилансера найти, чтобы вебсайт сделать. Казалось бы, и что тут такого? Уж кого-кого, а веб-девелоперов в стране хватает! За пару недель найду, - думал я, максимум за месяц. Как вы уже догадались, не нашел.

Этот лонгрид - о фрилансерах в частности, и немного о веб-разработке в целом, от лица заказчика. Я прекрасно понимаю, что большинство завсегдатаев Хабра - исполнители, а заказчикам тут периодически прилетает. И все же рискну высказаться. Надеюсь, будет весело.

Тут надо сделать небольшое лирическое отступление, чтобы вы смогли погрузиться в контекст. Итак, у нас с коллегами есть небольшой pet-проект в области образования. Если точнее - в области постановки произношения. Внутри этого pet-проекта есть еще один вложенный класс pet-проект, - YouTube канал, на котором мы тестируем разные идеи, собираем отзывы, и немного анализируем рынок.

Постепенно у нас накопилось приличное количество текстового материала - статьи, аналитика, разные тестовые задачки, и тому подобное. Параллельно YouTube канал набрал несколько сотен тысяч подписчиков. А еще мы стали замечать, что доля поискового и внешнего трафика на канале приближается к 50% (это много). Причем поисковые фразы хорошо коррелируют с "нужными" высокочастотными запросами.

В общем, мы дозрели до своего сайта.

Пишем техзадание

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

Сделать совершенно типичный сайт для публикации статей. Функциональные требования не выделяются чем-то уникальным. Дизайн - примитивный. Нагрузка - средняя. Pagespeed insights >= 90%.

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

Полнотекстовый [морфологический] поиск а-ля ElasticSearch или Sphinx.
Фильтрация списков статей по множеству тегов.
Сортировка списков статей по времени и/или категориям.
Кастомное меню с навигацией по ключевым статьям на главной странице.
И еще одна секретная фича на фронтенде.

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

Осталось разобраться с нефункциональными требованиями.

Гугл не скрывает, что использует скорость загрузки страниц в качестве одного из критериев своих алгоритмов ранжирования. Поэтому мы включили в ТЗ оптимизацию сайта до зеленой зоны PageSpeed Insights. Кроме вполне очевидной пользы, это требование позволяло нам отсечь неопытных исполнителей (спойлер: это сработало, но не совсем так, как мы предполагали).

За отправную точку оценки нагрузки взяли наш YouTube трафик. В хороший день это 7-8 тысяч уникальных посетителей со средним количеством просмотренных видео 2,4. Берем полуторакратный запас и получаем порядка 30 тысяч хитов. Это примерно 12 тысяч посетителей в день.

В общем, у нас получилось вполне приличное техзадание на 15 страниц.

Публикуем заказ.

Рассуждая в том ключе, что "хорошие" исполнители ТЗ читают, а "плохие" нам не нужны, мы добавили в техзадание "капчу": просьбу начинать отклик со слов "Hello there!".

Сам текст публикации был максимально коротким. Примерно таким:

Cделать контентный сайт на базе вашей любимой CMS/фреймворка согласно ТЗ

Внимательно прочитать требования к проекту (!).
Выбрать оптимальную CMS/фреймворк, удовлетворяющие требованиям.
Оценить общую трудоемкость и стоимость проекта.
Согласовать приблизительный поэтапный план разработки.
Собственно, сделать сайт согласно плану.

Заказ опубликовали на пяти площадках, с разницей по времени от нескольких дней до недели. Там, где было возможно, мы старались "выделить" объявление:

площадка

стоимость публикации

стоимость "выделения"

ИТОГО:

freelance.habr.com

0

800

800

fl.ru

350

1250

1600

freelancehunt.com

0

840

840

freelance.ru

350

0

350

weblancer.net

0

0

0

На все про все было потрачено примерно 3500 рублей. Бюджетненько!

Разбираем отклики

Всего на заказ отозвались ровно 100 исполнителей:

площадка

всего откликов

прошли "капчу"

прошли "капчу" %%

freelance.habr.com

33

8

24%

fl.ru

30

4

13%

freelancehunt.com

20

1

5%

freelance.ru

15

1

7%

weblancer.net

2

0

0%

ИТОГО:

100

14

14%

Как видно из таблицы, с откликами проблем не было. Была проблема с их качеством. Признаюсь честно, уже на второй день нас начала охватывать паника: подавляющее большинство откликнувшихся не читали задание! Более того, некоторые не читали даже сам текст публикации.

Чтобы себя успокоить, мы сделали трекер исполнителей с классификатором их видов:

Вид первый, "автобот":

Здравствуйте, это как раз мой профиль, готов помочь!
Портфолио > https://***.**/portfolio/
Телефон/Viber/WhatsApp: ***

(здесь и далее приведены совершенно реальные отклики, орфография и пунктуация авторов сохранены)

Характерные признаки "автобота": присылает отклик практически сразу после публикации заказа. Где-то в интервале от минуты до пары часов. Как правило, в портфолио бота половина ссылок не работает. Причем это лучшая половина. Чаще всего боты водятся на freelancehunt.com и freelance.ru. Что бы вы ни делали, вступать с вами в переписку бот не будет.

Вид второй, "разработчик продающих сайтов":

--не бот--

***, добрый день! Меня зовут ***, я веб-разработчик с опытом более 10 лет. Самое главное - это продающая структура, а уж потом дизайн и верстка, я делаю именно "продающие" сайты, которые реально работают.

CMS на которых делаю сайты: 1. 1С Битрикс 2. Тильда 3. Wordpress

Характерные признаки: старательно дистанцируется от ботов. Вне зависимости от задачи, использует такие словосочетания как "повышение продаж", "продающая структура", "конверсия", "SEO", и другие. Средний опыт работы - более 10 лет. В списке регалий имеет Битрикс и Тильду.

Наконец, вид третий. Самый забавный. "эффективный project manager":

Добрый день, ***!

Бюджет вашего проекта 10 000$. Если вас устраивает такая стоимость, то давайте назначим звонок на конкретное время. Длительность звонка: 1 час. Наш CTO к тому времени ознакомится с вашим документом и подготовит ответы на ваши вопросы.

Характерные признаки: управляет вселенной, не привлекая внимания санитаров технических специалистов. Может за пару минут оценить любой проект, не открывая ТЗ. Всячески демонстрирует свою занятость, но при этом подчеркнуто избегает какой-либо конкретики. Очень любит круглые суммы.

В итоге, лучше всего себя показал freelance.habr.com (ура!), хуже всего - freelancehunt.com и freelance.ru. Как еще существует weblancer.net - непонятно.

Далее, из сотни откликнувшихся лишь 14 человек полностью прочитали наше задание. Еще шестеро капчу не прошли, но попали в short-list, потому что написали развернутый отклик.
Итого - 20.

Слава роботам!

Пытаемся планировать

Life is what happens to you while you're busy making other plans
John Lennon.

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

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

Почти половина отвалилась сразу, даже не попрощавшись. Причем, что интересно, отвалились как раз те, чьи отклики производили наилучшее впечатление.

Несколько человек все же планы прислали, чем нас еще немного повеселили. Ниже - наш топ-3.

"Стандартный" план:

этапы разработки у нас стандартные ))
1. Разработка прототипа проекта (UX проектирование)
2. Разработка дизайна
3. Верстка
4. Посадка верстка и доработки серверной части
5. Тестирование

Еще вариант, со сроками:

1. Прототипирование и создание дизайна сайта 10 дней
2. Верстка сайта 7 дней
3. Программирование структуры сайта 30 дней
4. Создание уникальных модулей 10 дней

И, наконец, мой любимый, самый короткий:

Разработка фронтенда, админ-панель, бэкенд
По срокам примерно 2-3 недели, цену предлагайте сами.

В результате, этап планирования прошли шестеро. Трое - с Wordpress, и по одному с Laravel, October CMS, и Drupal.

Впереди нас ждет еще очень много всего интересного!

Wordpress - три попытки, от $2000 до $4000

Когда-то в начале двухтысячных Wordpress задумывался как простая система для self-hosting блога: удобная админка, таксономия, теги, антиспам фильтр, неплохой по меркам тех времен редактор, и так далее. Где-то начиная с 2011, стали появляться разнообразные плагины: e-commerce, booking, формы, галереи, и так далее.

Наверное, это был правильный вектор развития с точки зрения бизнеса. В конце концов, миллионы владельцев "лэндингов" и интернет-магазинов не могут ошибаться. Вот только как система для публикации контента Wordpress так и остался где-то в 2010-м. И даже, по-моему, немного откатился назад.

Камнем преткновения для Wordpress разработчиков стали наши требования по скорости загрузки страниц. Напомню, что нам очень хотелось получить сайт в зеленой зоне PageSpeed Insights. Так вот, по оценке наших кандидатов, оптимизация Wordpress съедала 40-50%% бюджета. Буквально каждый кандидат категорически настаивал на разработке кастомизированной темы "с нуля".

Вырисовывалась примерно такая логика: вы должны использовать Wordpress, потому что это самая популярная CMS, и там много классных тем и плагинов. Только вам эти темы использовать нельзя, потому что работают они медленно. А так как ядро CMS рассчитано на вот это вот все, то надо понимать. Поэтому, если хотите сделать хорошо, то надо делать кастомную тему, то есть доплатить за "оптимизацию" Wordpress.

Мы просто подсчитали, что только оптимизация обходилась нам где-то от $800 до $2000. Но это еще не самое страшное.

Другая проблема Wordpress, как ни странно, - это очень скромные "встроенные" средства управления контентом. Приведу такой пример: на Хабре можно посмотреть список статей в хабе фриланс, либо в хабе разработка веб-сайтов, но нельзя получить список статей, относящихся к обоим хабам одновременно.

У нас, конечно, гораздо меньше контента, чем на хабре. Зато время его жизни существенно больше. Чтобы помочь пользователям ориентироваться во всех наших статьях, мы решили добавить фильтры по множеству тегов.

Если вы хотя бы чуть-чуть знакомы с основами баз данных, то набросать запросы для фильтров many-to-many сможете за полчаса А вот стандартных плагинов для Wordpress с такой функциональностью - нет. Как нет и полнотекстового поиска из коробки, не говоря уже о морфологическом.

В общем, у нас были три Wordpress-кандидата. Все трое - хорошие специалисты с неплохими портфолио и впечатляющим опытом. Кстати, говоря о бюджете, даже с учетом Wordpress-оверхеда, он был вполне приемлемым: от $2000 до $4000.

Мы всячески пытались довести хотя бы одного из кандидатов до контракта. Но не сложилось: первый сошел с дистанции в процессе согласования деталей. Второй изначально особой активности не проявлял, как бы намекая, что не сильно заинтересован. Третий немного подумал, и сам предложил отказаться от Wordpress в пользу Laravel. За +20% к цене.

А ведь все так хорошо начиналось!

Laravel - $2000

Параллельно у нас появился еще один многообещающий кандидат:

Hello there! Я не могу предложить готовую CMS, но зато могу предложить потрясающий фреймворк Laravel. На нём могу реализовать весь необходимый вам функционал.

Вечер перестает быть томным, - подумали мы.

Буквально за выходные кандидат-ларавел развёл кипучую деятельность: написал весьма подробное техническое обоснование, включая примерный расчет VPS сервера под требуемую нагрузку, список необходимых сторонних библиотек, краткий план работ, mind map, и даже прислал несколько референсных тем. Несмотря на разницу во времени, кандидат отвечал на вопросы практически мгновенно, да и в целом производил самое что ни на есть благоприятное впечатление.

Вот только был один нюанс: у парня не было портфолио. Совсем не было. Ни единого сайта. Более того, он отказывался показать хоть какие-нибудь примеры своих работ, ссылаясь на NDA.

И тем не менее, мы решили рискнуть. Во-первых, кандидат-ларавел был самым коммуникабельным из всех, с кем мы на тот момент успели пообщаться. Во-вторых, сумма ($2000 за основную функциональность) и сроки (4 недели) выглядели вполне разумно. Поэтому мы предложили вариант с поэтапной оплатой через escrow. От него требовалось только подтвердить план на первые два этапа и заключить контракт

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

А жаль, нормально же вроде общались.

Drupal - $8000

В любом pet-проекте есть несколько интересных идей "на вырост". У нас они тоже есть. Что гораздо важнее, мы знаем, чего мы делать не будем: не будем открывать интернет-магазин, запускать корпоративный портал, хостить форумы, и тому подобное.

Короче говоря, в свете наших более чем скромных планов, целесообразность использования монструозных CMS типа Drupal, выглядела несколько сомнительной.

Поэтому мы немного удивились, когда на наш заказ откликнулась целая web-студия с предложением сделать сайт на Drupal 9. Они оценили бюджета проекта в $8000.

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

Как были обоснованы $8000? А никак! Все, что было хоть сколько-нибудь детально расписано, относилось к дополнительным опциям. Ну, вы понимаете, все эти , "структура сайта", "user story", "подготовка ТЗ" и "фиксация деталей по функциям". Где-то даже промелькнула фраза "наполнение контентом".

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

В общем, от этого предложения мы отказались сами: писать user story нам не хотелось, да и в целом выбор Drupal показался необоснованным.

А еще мы пожалели $8000.

October CMS - $2000

Нам изначально нравилась философия минимализма в October. К примеру, мы не планировали регистрацию внешних пользователей. Для простого контентного сайта в этом просто нет необходимости: контент будем добавлять только мы, а для комментариев все равно удобнее использовать что-то вроде commento.io. Зачем нам лишний код?

В October из коробки нет совершенно ничего лишнего. С другой стороны, почти все, что нужно, можно найти в виде плагинов. Включая ту же регистрацию пользователей. Сами мы, конечно, делать что-то на October вряд ли бы стали, но ведь поэтому мы и ищем веб-разработчика!

Мы хотели пообщаться с кем-нибудь с опытом в October, хотя статистически это и было маловероятно. И все же, иногда даже маловероятные события случаются.

В отличие от кандидата-ларавел, наш кандидат-october старался избегать телефонных разговоров. Зато он много писал. В частности, он написал самый детальный план из всех, что мы видели, включая целых два списка необходимых плагинов (основной, и "резервный"), расчет сервера, прикидку по "кастомной" функциональности, по доводке дизайна, оценку рисков, и еще много всего.

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

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

В общем, мы заключили контракт и закинули деньги за первые два этапа на escrow.

Первый этап был закончен точно в срок, мы даже удивились. Не то, чтобы это был сложный этап, - изначальная установка CMS, базовых плагинов и тем. Но к этому моменту мы уже так устали от всего происходящего, что радовались как маленькие дети. У нас, наконец, что-то заработало! Мы даже можем что-то опубликовать!

Увы, наша радость была недолгой. Второй этап был отложен на неделю, потом еще на неделю. Судя по коммитам на Github, второй этап даже не был начат. А потом исполнитель просто пропал.

Запил, наверное.

Бонус - профессиональные студии, до $18 000

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

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

Если коротко, то там еще хуже.

Нам присылали "коммерческие предложения", скопированные с других проектов, не имеющих ничего общего с нашим. Нам предлагали сделать магазин, - "вам же когда-нибудь понадобится магазин?" Нам предлагали "зашифровать чувствительные данные". Нам предлагали увеличить трафик в два раза (серьезно?!?), а также "переделать сайт под SEO продвижение", и "протестить нишу". Да, еще нам предложили "написать адекватное ТЗ". Я уже рассказывал про use cases и user story?

Нам долго и обстоятельно рассказывали о преимуществах EditorJS над Tailwind, и TinyMCE над Bootstrap (и наоборот). Нам даже объяснили, почему монолитная архитектура нам подходит лучше, чем микросервисы. Ах да, ну и конечно, правильный PHP - это PHP 7, правильный Laravel - это 8, ну а правильный HTML - это 5! И никак иначе!

Зачастую все это сопровождалось огромным количеством грамматических ошибок и перемежалось фразами "я вас услышал", "наиболее оптимальный", и тому подобными. Не поймите меня неправильно, лично я спокойно отношусь к грамматическим ошибкам. Но когда в твоей команде большинство - лингвисты, им сложно объяснить, почему project-manager подрядчика пишет как третьеклассник, а разговаривает как пятиклассник.

И все это за каких нибудь $18 000 (восемнадцать тысяч долларов США). Ну хорошо, если мы "урежем" часть требований - $12 000. Но тогда работа аналитика оплачивается отдельно.

Вместо эпилога

За время поиска веб-разработчика наши контент-менеджеры и редактор подготовили 15 новых статей. Их по-прежнему негде публиковать.

Подробнее..

Нетехнические вызовы Open Source разработки

26.03.2021 12:06:50 | Автор: admin

Мы все любим ПО с открытым кодом. Журналисты и ученые визуализируют и обрабатывают данные с FOSS (Free and open-source software), государства переводят спонсируемые ими разработки на свободные лицензии, активисты приватности постоянно совершенствуют безопасность технологий потому, что в код смотрит много глаз. Базы данных, серверное обеспечение и прочая инфраструктура на открытом коде обеспечивает работу миллионам серверов по всему миру. На FOSS держится весь наш любимый интернет. И я не говорю о том, что на FOSS основана экосистемы крупнейших движков сайтов: WordPress, Joomla и Drupal. С помощью FOSS миллионы людей могут перевести ПО на самый редкий язык и не дать этому языку умереть под влиянием экспансии мировых языков. Наконец, идеология FOSS дает ответ и монополизации социальных сетей с помощью ActivePub/Fediverse.

Можно достаточно долго перечислять преимущества ПО с открытым кодом, но, по какой-то причине, мы видим, что FOSS повсеместно встречается с противодействием, часто откуда не ждали. В преддверии онлайн-конференции Админка, которая пройдет завтра и послезавтра, я постарался проанализировать нетехнические вызовы Open Source разработки.

Растущие требования пользователей к UX

В OS много программистов, но мало UX-еров. Из-за этого много OS-приложений выглядят совсем не так как их коммерческие аналоги? В какой момент OS-движение оттолкнуло дизайнеров и разработчиков? Или, может быть, их никогда не воспринимали всерьез, потому что мышка для слабых, командная строка и вот это все.

Но даже если UX-еров пригласили, то как поставить процесс так, чтобы UX-разработка не превратилась в мучительную долгоиграющую драму, как например, с Gutenberg в WordPress'e? Можем ли мы говорить о том, что человечество вообще достигло определенных ограничений того, что волонтерские самоорганизованные группы могут разработать? Т.е. есть ли какой-то физический когнитивно-организационный предел open Source разработки? Я верю, что нет, но вопрос, так или иначе, оставлю.

Высокий входной барьер для повсеместного использования

Многие платформенные решения глубоко внутри опираются на Open Source инфраструктуру, но постоянно бравируют тем, что мол "нас не надо устанавливать, зачем вам маяться с OS". И действительно, FOSS часто сложен в установке и поддержке.

Я не говорю тут о каких-то пользовательских FOSS-продуктах (например, Tor и VLC). А вот когда у вас предприятие малого или среднего бизнеса (МСБ), но нет тех.образования (и желания его получать тоже нет) в 90% вы не выберете Open Source.

XKCD Comic #196. Источник: https://www.explainxkcd.com/wiki/index.php/File:command_line_fu.pngXKCD Comic #196. Источник: https://www.explainxkcd.com/wiki/index.php/File:command_line_fu.png

И, наверное, можно сказать, что ну и на фиг представителей МСБ, не место им в наших достойных кругах. Но, FOSS это не только софт, это и практика создания, практика использования, практика взаимодействия. И, на мой взгляд, круг практикантов следует расширять.

В долгосрочной перспективе снобизм может сыграть злую шутку над всем FOSS-движением, т.к. вырастет пара поколений (а кто-то из них даже получит тех.образование), которые вообще не поймут, что делать с FOSS и куда его втыкать.

Программирование это новый алфавит и новый стандарт нормы грамотности или все-таки специализированное знание и дорога к преумножению скорби?

Финансовая устойчивость разработчиков OS-приложений

Наконец, вопрос денег. FOSS это не только ПО, не только практика создания и пользования, но и определенная идеология. И экономическая доступность FOSS как сила этой идеологии, в какой-то момент превращается в слабость в частности, в стигматизацию бизнес-моделей, основанных на FOSS.

Корпорация зла, пособники капитализма, продажные разработчики, негодяи, технология двойного назначения и т.д. каждый из этих эпитетов легко словить даже за намек на freemium или какой-то более креативный подход к заработку с использованием Open Source.

А те, кого сообщество готово превозносить, вынуждены согласиться на (или полностью принять) аскезу, чтобы не вызвать огонь критики. Вернер Кох, создатель GnuPG, должен сначала признаться о плачевном положении вещей, чтобы получить донаты со всего мира и сохранить место в пантеоне FOSS.

Тот самый новостной заголовок, привлекший внимание к Вернеру КохуТот самый новостной заголовок, привлекший внимание к Вернеру Коху

FOSS это только для гуру, аскетов и святых? Или для простых людей с семьями и долгами тоже?

Эпилог

Безусловно, эти вызовы не единственные. Их много. Но несмотря на все вызовы. Мы любим и верим в Open Source. И нам надо поговорить. Давайте встретимся на этих выходных онлайн и послушаем и поговорим про разные сюжеты из жизни Open Source разработчиков.

Модератор конференции Олег Северов, Computer Vision Developer, один из создателей проекта Lacmus.

Организатор блистательная Алиса Цветкова, Теплица социальных технологий.

Участники: Дмитрий Винокуров, координатор Linux сообщества города Пермь; Антон Булычев, создатель проекта Amnezia VPN; Олег Понфиленок, основатель Copter Express; Элина Ахманова, Associate Software Engineer в Red Hat; Ксения Ермошина, PhD (Mines ParisTech), сотрудница Центра Интернета и Общества (CNRS, Франция), UX-исследователь в Delta.Chat; Святослав Игуана, мейнтейнер проекта NewsViz; Дмитрий Якимчук, веб-разработчик; Ваш покорный слуга, Николай Лебедев, Ион Бурдианов (команда разработки Теплицы); Анна Ладошкина (Foralien Bureau); Dr. Quadragon, евангелист Mastodon; Георгий Перевозчиков, проект Lacmus; Ивана Бегтин, директор АНО Информационная культура; Вадим Мисбах-Соловьев, технический консультант РосКомСвободы и др.

Обложка конференции АдминкаОбложка конференции Админка
Подробнее..

Перенос форума IPB в bbPress WordPress

03.10.2020 00:12:20 | Автор: admin

"Invision Power Board" он же "Invision Community", я его назваю IPB.

Форум IPB хороший, функциональный во всех отношениях, даже больше того. Даже больше того - это практически полноценный CMS движок для сайта, взамен WP и подобным.

Но я давно использую для нескольких проектов CMS WP, хотя и люблю когда сайт написан с нуля без лишнего мусора. Примеры моих проектов для сравнения:

  • lagonaki.ru - CMS WP + серьезная букинг тема, дописанная-переписанная, симбиоз CRM с Bitrix24. Постоянно обновляемая + множество плагинов, которые так же обновляются. Обслуживается самостоятельно, нужная информация вносится без привлечения внешних разработчиков.

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

CMS WP как раз удобен тем, что он замечательно модернизируется, обновляется и поддерживается большинством плагинов. Например, нужен вам интерфейс для интернет коммерции - пожалуйста. Нужен плагин для SEO - пожалуйста. Нужен плагин для оптимизации - пожалуйста - несколько сот вариантов, и т.д. и т.п. В общем, лирика, теперь по делу.

Назрела пора реанимировать замороженный туристическо-альтруистический проект fisht.ru, у которого один из разделов "Форум" forum.fisht.ru, но жил он своей жизнью от основной части сайта. Закрыл форум вынуждено на регистрацию новых пользователей 3 года назад из-за обилия спамеров и отсутствия решения по борьбе со спамом. Предлагалось только обновить движок и заплатить за это 700$ + русификация...

Сейчас, начал изучать как можно объединить проект под WP и перенести форум с IPB платформы.

Наткнулся на cms2cms.com - проект, который конвертирует много форумов в другие форумы. Автоматизация процесса и техподдержка. Интересно, но очень дорого... Тем более автоматизация неважно сработала на первом этапе.

CMS2CMS - сайт для миграции форумов. Но дорогой :)CMS2CMS - сайт для миграции форумов. Но дорогой :)

Свои прогеры по горло загружены, решил поискать исполнителя на "Фрилансер". Нашел толкового парня, но он специалист в IPB, решает вопросы с модернизацией, дизайном, обновлением и т.д. Если нужно - обращайтесь к нему, зовут Олег. Ему огромное спасибо, за то что решил оперативно помочь, но я все же хотел не на IPB остаться, а именно с него "съехать". Много причин, но две основные "Лицензия" дорогая и разделение сайта на форум и сайт. Олег и подсказал, что оказывается есть возможность съехать стандартными средствами bbPress. Вот та самая статья: Invision IPB v3.1x, v3.2x, v3.3x & v3.4x Importer for bbPress. За что ему отдельное спасибо, люблю когда не навязывают свою услугу, а показывают как в действительности обстоят дела.

Установка bbPress и перенос данных из IPB

Работающая версия на 02.10.2020. Если хотите сэкономить себе часы, а может даже и дни свободного времени, то рекомендую воспользоваться работающей связкой версий WP и bbPress:

Долго мне пришлось разбираться, чтобы понять, что последняя версия Wordpress 5.5.1 и предыдущие версии 5.4 не идут с модулем bbPress 2.6.5, который обновлялся 2 месяца назад. В общем, это основная сложность, которая съела уйму времени.

Далее, активируем плагин bbPress, заходим в "Инструменты" - "Форумы" - "Импорт форумов" и выбираем платформу "Invision", далее по вашим настройкам. Там все дальше просто.

Плагин "bbPress" - Инструменты - Форумы - Импорт форумовПлагин "bbPress" - Инструменты - Форумы - Импорт форумов

Если сервер с которого вы будете переносить форум находится на одной площадке, то указываем localhost, если же другой, то, соответственно, тот, с которого переносим.

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

Если у вас идет вот такая картинка, значит, перенос производится правильно!

Производится перенос форума IPB в bbPressПроизводится перенос форума IPB в bbPress

Если заметили, что все повисло там, где не должно было виснуть - нажмите паузу и потом запустите далее. Сразу в phpMyAdmin в вашей базе увидите, что цифра начала увеличиваться, значит процесс переноса идет.

Для кого вообще эта статья?

Этот статью написал для тех, кто будет искать выход переноса форума IPB на платформу WP. Я выбрал bbPress, т.к. это по сути создатели WP - оригинальная интеграция всегда лучше. Хотя отсутствие обновлений, у меня "съело" очень много времени...

Если используете nginx

Рекомендую сразу внести правки в конфигурационном файле nginx
Требуется установить в location @fallback - для http и для https

    proxy_connect_timeout       600;    proxy_send_timeout          600;    proxy_read_timeout          600;    send_timeout                600;

В противном случае, на определенных операциях настройки форума будет выдаваться ошибка. В частности у меня постоянно выдавалась ошибка, если я в bbPress "Инструменты" - "Форум" - "Восстановление форума" запускал процесс "Пересчет темы для меток тем", то операция уходила и заканчивалась "504 Gateway Time-outnginx/1.14.1".

Также огромное спасибо Никите Максименко и Егору Шалаев из службы тех поддержки TimeWeb, которая поучаствовала в запуске новой жизни проекта.

Буду признателен комментариям от тех, кто сталкивался детально с настройкой bbPress, т.к. один вопрос еще не решил. В частности это ошибочно считает темы и посты, нужно пересчитать, все никак не получается.

Ошибка в количестве тем и сообщений.Ошибка в количестве тем и сообщений.

Что интересно, если создать ФОРУМ, в него потом добавить другой подфорум и потом его вывести из-под него, то в новом созданном форме остается тоже самое количество из подфорума. Пример, я создал форум "Горы", в него перенес "Общие обсуждения", после "Общие обсуждения" вывел из "Горы" итог - равное количество Тем и сообщений. В общем, все, кроме этого, уже нормально и разобрался. Помогите, если кто-то сталкивался с этой проблемой.

Подробнее..

Почему собственный образ ISO самое оптимальное решение для своего сервера

05.11.2020 12:07:06 | Автор: admin
Подняв сервер, можно сразу поставить одну из стандартных ОС, которые предлагает хостер. Но есть и другой вариант загрузить собственный образ ISO и установить из него произвольную ОС и любой софт на свой выбор.



Это реально очень удобно. Мы можем поставить на сервер ParrotOS со всеми утилитами для пентестинга, готовый файл-сервер или любую ОС, даже Android или MacOS. Можно поставить специально подготовленную систему, настроенную именно для наших задач.

Зачем это нужно? Вот несколько примеров.

Установка системы из ISO самый эффективный метод, если для сервера требуется нестандартное программное обеспечение и ОС. Всё-таки не каждому требуется именно типовая установка Nginx/Apache и стандартный набор программ из стека LAMP/LEMP/LEPP. Кому-то сервер нужен для других целей. Здесь много вариантов. Скажем, мы хотим запустить игровой сервер, медиасервер, групповой чат или набор утилит для пентестинга, то есть тестирования веб-сервисов на предмет уязвимостей, чтобы сообщить о них владельцу сервиса и, например, получить положенное вознаграждение по программе bug bounty.

Пентестинг


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

Для пентестинга лучше всего подходят операционные системы вроде Kali Linux и ParrotOS. Соответственно, их удобнее всего устанавливать из образов ISO.

Kali Linux


Kali Linux включает инструменты для следующих задач:

  • Обратная разработка (реверс-инжиниринг). Средства для отладки программ и реверс-инжиниринга исполняемых файлов.
  • Стресс-тестирование. Инструменты для стресс-тестирования сетей и веб-сервисов. По сути это DDoS системы, но не с целью обвалить её, а с благими намеренями выяснить, какую нагрузку система способна выдержать.
  • Взлом оборудования. Инструменты для работы с Android и Arduino.
  • Судебная экспертиза (форензика). Инструменты для цифровой криминалистики. Они позволяют создавать образы дисков, проводить анализ образов памяти и извлекать файлы.

Для упрощения пентестинга в системе есть категория Top 10 Security Tools (Топ-10 инструментов безопасности). В эту категорию входят aircrackng, burp-suite, hydra, john, maltego, metasploit, nmap, sqlmap, wireshark и zaproxy. Это самые известные и эффективные инструменты.

Например, Metasploit фреймворк с огромной коллекцией эксплоитов для всех известных уязвимостей, швейцарский нож пентестера. Nmap идеальная программа для сканирования портов. Wireshark лучший снифер и анализатор сетевого трафика. John the Ripper известный инструмент для брутфорса паролей, и так далее. Всё это поставляется в комплекте Kali Linux, так что ничего не нужно инсталлировать вручную. Всего в комплект входит более 600 предустановленных хакерских приложений. Это удобно. Хотя, в принципе, ничто не мешает установить любые нужные программы в любом другом дистрибутиве Linux.

Свежие образы Kali Linux генерируются каждую неделю и выкладываются на официальной странице.

Как вариант: Parrot OS


Дистрибутив Parrot OS на базе Debian служит той же цели, что и Kali Linux. Это своего рода портативная лаборатория для всех операций, связанных с ИБ, от пентестинга до форензики и стресс-тестирования, но при этом позиционируется также для разработчиков программного обеспечения с упором на безопасность и приватность.

Kali Linux и Parrot OS во многом похожи, но отличаются системными требованиями (320 МБ ОЗУ для Parrot OS, 1 ГБ для Kali Linux), внешним видом (MATE и KDE у Parrot OS против GNOME у Kali Linux) и набором хакерских инструментов. В Parrot OS есть всё то же, что и в Kali Linux, плюс дополнительно собственные инструменты, такие как AnonSurf (удаление всех своих следов из браузера) и Wifiphisher (установка поддельной точки доступа WiFi для обмана окружающих устройств). По производительности Parrot OS тоже выигрывает, особенно на слабой конфигурации, где Kali Linux начинает заметно лагать.

Различные варианты образов ISO и докер-контейнеры с Parrot OS выкладываются здесь.

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

Игровой сервер


Если нужно поднять игровой сервер, во многих случаях тоже удобнее загрузить специализированный ISO и установиться из него.

Например, игра Counter-Strike Global Offensive самый популярный в мире шутер. Есть несколько причин для установки собственного приватного сервера. Здесь вы можете ограничить круг игроков, полностью исключить читеров. И самое главное вы устанавливаете собственные правила игры.

CSGO Server устанавливается на Linux, на Windows 10 или другую систему. Но может быть удобнее сделать ISO, где всё уже установлено, включая SteamCMD, консольный клиент Steam, который является обязательным условием для установки сервера CSGO. Так что если вы всё-таки устанавливаете CSGO сразу на сервер, то сначала нужно поставить SteamCMD. Если подготовить образ ISO со всем необходимым, то в дальнейшем его можно использовать многократно, накатывая на разные серверы.

Некоторые хостеры предлагают уже готовые серверы CS. Их можно арендовать как с оплатой за слоты, так и с оплатой за ресурсы (профессиональные тарифы игровых VDS/VPS).

Или взять Minecraft. Да, это детская игра, но она входит в пятёрку самых популярных онлайн-игр в мире. Для Minecraft-серверов создана специальная MineOS Turnkey, которая распространяется в виде ISO. Хотя всё необходимое программное обеспечение можно установить отдельно на существующий сервер BSD/Linux, но гораздо приятнее сразу накатить систему со всем необходимым. Система изначально сконфигурирована и оптимизирована под эту задачу: здесь есть инструмент администрирования сервера, управление резервными копиями, продвинутые инкрементальные бэкапы игрового мира (резервное копирование rdiff с использованием алгоритма rsync), загрузка пользовательского интерфейса и серверных модов (vanilla, bukkit и др.) одним щелчком мыши.




MineOS

MineOS Turnkey сделана на основе известной системы Turnkey Linux.

Turnkey Linux


Turnkey Linux библиотека готовых системных образов на базе Debian 8 ("Jessie"). Turnkey переводится под ключ, то есть всё включено. В каждый образ изначально интегрированы лучшие компоненты свободного программного обеспечения. В результате получаются безопасные и простые в использовании решения образы ISO для любых нужд, какие только могут понадобиться.


Каталог образов ISO на сайте Turnkey

Debian крупнейший дистрибутив GNU/Linux, в состав которого входит 37 500 пакетов опенсорсных программ. Однако большая часть этих программ малоизвестны, потому что не включены по умолчанию в состав стандартных дистрибутивов. Turnkey ставит своей миссией изменить это.

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

Turnkey это более 100 готовых к использованию образов. Все они поддерживают автоматическое ежедневное обновление с последними патчами безопасности, резервное копирование и восстановление одной кнопкой: система сохраняет изменения в файлах, БД и управлении пакетами в зашифрованном хранилище, из которого серверы могут быть автоматически восстановлены. Инструмент для резервного копирования TKLBAM встроен в ядро системы Turnkey Core.

Каждый дистрибутив Turnkey включает в себя веб-интерфейс управления, веб-оболочку и простую консоль конфигурации.

Все образы легковесные (от 150 МБ), тщательно собраны с нуля с минимально необходимым набором компонентов, чтобы обеспечить наиболее эффективное потребление ресурсов, эффективность и безопасность. Во всех релевантных образах SSL работает из коробки на бесплатных сертификатах Let's Encrypt.

Вот некоторые образы из коллекции Turnkey:

  • Стек LAMP (408 МБ VM, 378 МБ ISO). Типичный комплект Linux, Apache, MariaDB (вместо MySQL), PHP/Python/Perl.
  • NGINX PHP FastCGI (404 МБ VM, 373 МБ ISO). Альтернативный стек для веб-сервера.
  • WordPress (425 МБ VM, 392 МБ ISO). Одна из лучших платформ для публикации блога с тысячами доступных плагинов на любой вкус.
  • Ghost (740 МБ VM, 582 МБ ISO). Ещё одна опенсорсная платформа для публикации блогов, с красивым оформлением, простая в использовании и бесплатная, отличается высокой скоростью. Приложение написано на Node.js, клиент администрирования на фреймворке Ember.js, а темы на движке Handlebars.js.


    Ghost
  • ZoneMinder (531 МБ VM, 483 МБ ISO). Система видеонаблюдения для дома, офиса или любого места. Использует лучший софт, совместим с любыми камерами. Подходит для систем любого размера. Продвинутое распознавание движения и объектов в реальном времени (системы EventServer и zmMagik).
  • Файл-сервер (508 МБ VM, 461 МБ ISO). Простой в использовании файл-сервер, совместимое с Windows общее хранилище файлов с веб-менеджером. Полностью готовый файл-сервер поддерживает протоколы передачи файлов SMB, SFTP, NFS, WebDAV и rsync. Возможно как приватное хранилище файлов, так и общедоступное на публичном хостинге. Работает на основе Samba и WebDAV CGI.


    Файл-сервер (File Server)
  • Nextcloud (639 МБ VM, 558 МБ ISO). Хранение файлов, фотогалерей, календаря и многого другого. Сервер доступен с любого устройства через интернет. Тоже можно установить или в локальной сети с доступом через интернет, или на публичном сервере.


    Nexctcloud
  • GitLab (1,1 ГБ VM, 1,0 ГБ ISO). Единый сервер для всего жизненного цикла разработки программного обеспечения. От планирования проекта и управления исходным кодом до CI/CD, мониторинга и безопасности. GitLab предоставляет собой управление версиями на основе Git, упакованное с полным набором инструментов DevOps. По сути, что-то вроде GitHub, но на своём сервере и с более продвинутым функционалом.
  • Медиасервер (648 МБ VM, 587 МБ ISO). Полностью настроенный медиасервер индексирует все ваши домашние видео, музыку и фотографии, затем автоматически транскодирует на лету и транслирует этот контент для воспроизведения на любом устройстве. На сервере работает Jellyfish с веб-приложением для управления файлами, система совместима с Windows и различными протоколами передачи данных, включая SFTP, rsync, NFS и WebDAV. Как и в случае с обычным файл-сервером (см. выше), здесь файлами можно управлять как в приватном, так и в публичном хранилище, то есть транслировать видео для всех желающих в интернете, насколько выдержит аппаратная часть сервера.
  • MySQL (398 МБ VM, 368 МБ ISO). Известная опенсорсная СУБД, которая сейчас принадлежит корпорации Oracle. Это быстрый, стабильный, надёжный, простой в использовании и многопоточный сервер баз данных SQL. Строго говоря, на самом деле на сервере фактически работает MariaDB, типичная замена для MySQL, хотя у неё есть некоторые функции, которые отсутствуют у MySQL, так что можно мигрировать с MySQL на MariaDB, но зачастую невозможно вернуться обратно.
  • Торрент-сервер (607 МБ VM, 549 МБ ISO). Файловый сервер с интегрированным общим доступом к файлам. Файлы добавляются в список загрузки через простой веб-интерфейс, который позволяет удалённо управлять сервером. Особенно полезно для централизации общего доступа к файлам в общей сети. Включает антивирусный сканер.

    Пожалуй, этот образ лучше подходит для установки в локальной сети вместе с медиасервером, чтобы скачивать фильмы и сериалы из торрентов, сохранять их в хранилище и транслировать на все телевизоры, ноутбуки и смартфоны в доме.
  • phpBB (416 МБ VM, 384 МБ ISO). Самое популярное в мире опенсорсное решение для веб-форумов. Основано на модулях. Высокий уровень безопасности, многоязычный интерфейс и продвинутая административная панель с настройками всех функций форума.
  • Zen Cart (416 МБ VM, 384 МБ ISO). Сервер для управления интернет-магазином. Поддержка разных языков и валют.
  • AVideo (672 МБ VM, 616 МБ ISO), бывшее название YouPHPTube. Опенсорсный сервер для видеотрансляций, созданный по образцу YouTube, Vimeo и т. д. С помощью AVideo вы можете поднять собственный сайт с видеороликами, а также транслировать видео в прямом эфире. Среди некоторых функций импорт и кодирование видео с других сайтов непосредственно из интернета, поддержка мобильных устройств через адаптивный гибридное приложение, которое позволяет непосредственно транслировать видео с телефона через сервер на широкую аудиторию.
  • Mattermost (622 МБ VM, 569 МБ ISO). Опенсорсная альтернатива Slack на своём хостинге. Объединяет обмен сообщениями и файлами, работает на ПК и мобильных устройствах, встроенное архивирование и поиск. Mattermost из коробки интегрируется с целым рядом других веб-приложений и расширяется модулями, так что вы можете создавать кастомные функции поверх ядра на Golang/React.


Это лишь несколько примеров специализированных образов ISO из коллекции Turnkey. Загружаете такой образ на свой сервер, устанавливаете систему из него и у вас сразу есть настроенное приложение со всеми необходимыми компонентами.



Таким образом, технически на VDS можно поставить практически любую операционную систему с любыми приложениями, используя собственный ISO. Главное, чтобы система имела драйверы для работы с VirtIO устройствами. Если их нет в образе, то нужно их добавить самостоятельно.

Подробнее..

Пересядь с иглы WordPress на Static Site Generator и Headless CMS нивкакиестэки

25.06.2020 12:16:27 | Автор: admin
Что делать, если WordPress (WP) уже не вставляет, а сайт пилить надо? Кейс авторского блога на Static Site Generator (SSG) и Headless CMS (HCMS).

Разбираем достоинства связки SSG + HCMS для программистов, диджитал номадов и современных контент-мейкеров.

I. Я устал, я ухожу


image

Меня зовут Давид. Вот уже шесть лет я каждый день пользуюсь WordPress. Я устал от такой жизни. Дал себе обещание найти новые решения для создания авторского контента.

Так я наткнулся на Static Site Generator (SSG) и Headless CMS (HCMS), потыкался и влюбился.

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

Почему сравниваю с WordPress? Потому, что это платформа для контентных сайтов по умолчанию. Альтернатив с хорошей экосистемой плагинов практически нет. А те, что есть, абсолютно однотипные и имеют одинаковые проблемы/особенности: PHP, рендеринг на сервере, шаблоны, БД и т.д.

А почему не облачные сервисы типа Medium? Главная причина ограниченная кастомизация (невозможность добавлять код со своими функциями и плагинами).

II. Что такое SSG?


Static Site Generator это (чаще всего) консольная утилита, которая при запуске специальной команды берет шаблоны вашего интерфейса, данные из источника данных и создает из них HTML, CSS и JS файлы с контентом.

image

Для шаблонов интерфейса вы можете использовать широкий инструментарий: от React.js, Vue.js до шаблонизаторов типа Pug, EJS, etc.

Как источник данных для SSG вы можете использовать: любую Headless CMS (о которых чуть ниже), HTML или Markdown файлы, любой API, да даже WordPress!

image

Вот пример Markdown файла, вот пример шаблона на React.js, а вот пример страницы сайта, которая была из него сгенерирована.

Представим ситуацию: у вас в блоге 99 постов.

В случае с WordPress для написания 100-го поста вы создаете его контент в админке и сохраняете его в Базе Данных (БД).

Когда к вам на сайт заходит посетитель, WordPress на каждый запрос берет шаблон, берет данные из БД, объединяет их и отдает клиенту.

image

В случае с SSG, вы пишете контент 100-той статьи в одном из удобных для вас форматов (от Markdown, до Headless CMS или того же WordPress), вручную или автоматически запускаете генерацию данной статьи, которая в итоге превращается в готовые HTML, CSS и JS файлы с контентом.

image

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

Когда к вам на сайт заходит посетитель, сервер сразу же отдает ему уже заранее подготовленные файлы со статьей.

Никакого серверного кода, походов в БД и подобного топтания.

Вот здесь на staticgen.com можно посмотреть список разных SSG и их возможности.

Сегодня в качестве примера SSG я буду упоминать Gatsby.js, потому что мне было быстрее и удобнее всего работать с React.js.

III. Что такое Headless CMS?


Headless это не всадник без головы. В данном случае headless значит не имеющий интерфейса.

Но на деле, как минимум административная панель всегда идёт вместе с HCMS, а клиентскую можно сделать или купить, как Тему.

Поэтому на практике headless означает, что весь функционал системы доступен в формате API. Чаще всего, HTTP REST или GraphQL.

image

Это также можно назвать API-first подходом.

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

Дисклеймер. SSG и HCMS по факту являются составляющими понятия JAM-stack, но для упрощения, я решил опустить этот термин.

image

IV. А теперь поженим SSG + HCMS и сравним этого минотавтра с WordPress-подобными CMS


Чтобы рассмотреть достоинства SSG на фоне WordPress, поиграем в ролевую игру (гусары, молчать):

Вы современная команда разработки, которая пилит SaaS криптовалютный распределенный мессенджер. И пора начать вести свое комьюнити единомышленников, где вы будете устраивать холливары, мериться кармой/IPO и всячески деградировать. Поздравляю, вы созрели для своего корпоративного блога.

Ваши девелоперы, оторвавшись от создания нового frontend фреймворка, предлагают воспользоваться SSG + HCMS, поскольку да хрен его знает, они сами плохо понимают, но говорят, что это годная технология.

Вы уже почти согласились, КАК ВДРУГ в дверь влетает бородатый, вонючий, пропитанный алкоголем седовласый мужчина. Все сразу понимают это PHP разработчик по-умолчанию.

image

А что происходит, когда в комнату врывается PHP разработчик? Правильно, он начинает предлагать WordPress или 1С-Битрикс. Пропатченные версии кодеров кричат еще что-то невнятное про Ларавел, но там не разберешь.

Делать нЕчего, придется вступить с ним в словесную перепалку и решить, кто победит: мудрость пердящих дедов или пыл новомодных нубов.

P.S. Я одинаково ненавижу / люблю всех разработчиков, поэтому не воспринимайте близко к сердцу.

P.P.S. Однако на месте рубистов я бы вышел из чата.

V. Дизайн


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

В случае с WordPress у вас есть варианты:

1) Скачать Тему и начать вставлять в каждый css !important-ы и править полуживой js код.

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

2) Начать пилить Тему самому. Но для этого придется использовать PHP, CSS, HTML и рендеринг шаблонов на сервере.

image

Ваши разработчики полноприводные Full-stack-JS-гуру, а значит в разработке они не более 2-х лет (кто выдержит это дольше?) и в жизни не видели голый HTML и CSS.

Фрилансеров, которые будут разрабатывать тему, брать не хочется, потому что все знают: WordPress фрилансеры самый подлое племя. В аду Данте для них кипит котел с примордиальным супом в бескислородной среде при температуре 660 градусов Цельсия. Чего это я? Да они хуже гоблинов. Не было ни одного случая, когда они не обманывали своего наниматели и не сбегали с награбленным золотом.

И тут на вашу тусовку влетает SSG!

SSG (Gatsby.js, VuePress, 11ty, etc.) позволяет писать клиентскую часть на современной Frontend экосистеме, например, React или Vue.js. В крайнем случае шаблонизаторы типа Pug, EJS.

А значит можно забыть о голом HTML и писать на удобных современному разработчику инструментах.

Кроме этого, можно купить Тему для SSG и она тоже будет использовать библиотеки последнего писка моды frontend-а.

image

Итоги по дизайну:

WordPress: +2 балла за разнообразие Тем, среди которых иногда можно найти достойные, а еще балл за плагины кастомизации (site-builder).

SSG + HCMS: +4 балла за темы и при этом современный стэк для создания своего дизайна. Поэтому в вопросах именно уникального дизайна SSG точно выигрывает.

P.S. Да, WordPress можно затюнить и подключить webpack с шаблонизаторами, но это процесс болезненный и хрупкий в случае использования сторонних Тем. А SSG идет с современным стэком прямо из коробки.

VI. Кастомизация


Ок, кроме офигенного дизайна, мы хотим расширить наш функционал сервисами подписки, комментариями и даже магазином с продажей ИМБА ЭНЕРД кхм простите продажей своего мерча.

А еще к вам приходит задача добавить в блог чарты с графиком стоимости вашей компании на IPO, данные для которого лежат в вашей Cassandra и раздается все это богатство Serverless лямбдами с AWS.

В случае с WordPress первые три задачи решаются подключением соответствующих плагинов (стандартные комментарии, WpForms и WooCommerce).

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

  1. Чтобы дойти до WPForms пришлось перепробовать еще 2-3 плагина. Все имели или проблему с дизайном, или проблему с настройками.
  2. Если отваливается SMTP сервис, то вы никак и нигде не увидите об этом предупреждения. Вам придется самому время от времени лезть в логи и проверять в чем дело.
  3. Подключение сторонних сервисов вроде MailChimp обычно доступно только в платной версии плагинов форм.
  4. Чтобы сделать нотификацию с Telegram, приходится качать очень мутный плагин и надеяться, что он будет работать, поскольку алертов про отвалившийся VPN вам не видать (на 18.06.20 слава богу VPN не актуален).


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

Еще нужно не забывать про жертвоприношения богам святого Рандома, чтобы какой-нибудь из плагинов (например email и Telegram, как было у меня) не вступил в перепалку и не порушил всю систему при следующем обновлении.

WooCommerce вообще считаю очень годным решением, поэтому шуршать кульком в его сторону не буду. Да, есть свои проблемы, но проблемы есть с любым eCommerce.

Итак, все вопросики решаются до момента с чартами.

Чтобы решить его, придется что-то напиливать на PHP, HTML, CSS и какой-то библиотеке графиков, которая требует при этом еще JQuery

Это будет долго, тяжело и больно.

image

Ок, а как тут справится SSG + HCMS?

Во-первых, SSG и HCMS устроены так, что их легко и удобно интегрировать с любой системой, которая имеет API.

Во-вторых, существует куча готовых плагинов.

image

Нужны комментарии? Берете любой из существующих сервисов (Disqus, Isso, Commento), интегрируете его. Можно даже подключить готовый плагин комментариев, которые сохраняются в Git репозиторий.

Нужна обратная связь/подписка? Берете любой MailChimp, SendGrid, TypeForm, пилите пару React форм, подключаете готовый API, та дааааам.

Интернет магазин? SSG спокойно можно подключить к Stripe, Paypal, Shopify или более бушкрафтовые решения типа ReactionCommerce / Saleor.io.

А еще всегда можно воспользоваться Redux или Firebase для написания более сложной логики корзины на фронте.

Чарты? У нас с вами в руках React/Vue.js, мы просто берем под него библиотеку чартов, делаем обычный HTTP запрос и отображаем данные easy.

+ Все эти плагины можно сделать статически сгенерированными, что будет огромным плюсом для SEO.

Итоги кастомизации:

WordPress: +3 балла 1 балл за кучу готовых плагинов, которые позволяют собрать все в одном месте, если для вас это важно. 2 балла за WooCommerce, годную площадку для российского интернет-магазина.

SSG + HCMS: +3 балла балл за удобное подключение любого сервиса через SDK + балл за отсутствии попытки собрать все в одном месте, что уменьшает количество багов и несовместимостей + балл за большое количество плагинов под SSG.

Тут я решил соблюсти равновесие, потому что подходы АБСОЛЮТНО разные и каждый решает задачу по-своему.

VII. Размер имеет значение


А здесь начинается несколько пунктов, где связка SSG + HCMS безоговорочно доминирует над WordPress-like решениями.

С помощью SSG + HCMS мы можем полностью управлять дизайном на современном стеке; можем легко интегрировать дополнительные плагины без потребности вшивать их куда-то глубоко внутрь системы. На выходе получаем оооочень оптимизированный и легкий бандл HTML, CSS, JS и других ассетов.

image

Более того, у многих SSG есть встроенные плагины по автоматической оптимизации картинок, svg и другой статики (lazy-loading, обрезка, сжатие, etc.).

В вебе размер имеет значение, я так думаю.

Меряемся размерами:

WordPress: -1 балл просто никуда не годится минификация и компиляция исходников на WP это боль. И чем больше вы добавляете плагинов, тем больнее. Довести WP до нормы размера бандла очень сложная задача.

SSG + HCMS: +2 балла здесь все и сразу из коробки, вообще не надо ни о чем думать. Значит, размер будет самым минимальным из возможных.

Важно: SSG на React.js или Vue.js дает больше гибкости с точки зрения написания кастомного функционала, но с ними бандл получается достаточно большой. Поэтому, если для вас критична именно скорость, лучше воспользоваться SSG на шаблонизаторах, например 11ty.

VIII. Скорость


А теперь просто киллер-фича: SSG позволяет выгрузить весь контент в заранее собранные HTML файлы минимального размера, что уже дает максимальную скорость показу сайта клиенту, так?

Но ведь вы при этом можете выложить эти HTML на любой облачный распределенный CDN.

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

Такой скорости клиентским витринам WordPress и другим CMS вообще никогда и не как не добиться.

Минимальный размер размер бандла и хорошая скорость: наш сайт будет невероятно технически SEO-оптимизирован.

Итоги скорости:

WordPress: -2 поскольку эта паскуда очень медленно работает.

image

SSG: +10 быстрее быть не может.

Важно: Конечно, у нас есть wp-cache. Но кроме его настройки и проблем с инвалидацией, вы по-прежнему располагаете сайт на одном конкретном сервере, что вообще несравнимо с распределенным CDN.

IX. Безопасность


Есть у меня дядя из Армении (а любой знакомый мужчина старше тебя в Армении считается твоим дядей, если твои волосы тоже черны и лихо завиваются густыми прядями в труднодоступных местах), учредитель фирмы кибер безопасности. Однажды он рассказал, как продает услуги кибербезопасности:

Я захожу к владельцу престижного онлайн-издания в Армении. И задаю ему всего одни вопрос: что будет, если ваш сайт взломают хакеры из {вставьте любую другую кавказскую страну, они там все питают такую личную неприязнь друг к другу} и в одном из постов годом ранее поменяют текст на призыв напасть на своих соседей?

После такого питча, директор любого СМИ быстро достает мешок денег и заказывает его услуги.

image

В случае с нашим блогом ставки не такие большие, но мы точно не хотим, чтобы каким-либо образом нас взломали и выкрали данные из CMS.

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

Но еще большая дыра в безопасности это эксплойты плагинов, которые авторы очень редко лечат.

И так будет всегда.

image

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

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

SSG же компилирует все заранее и готовым HTML страницам нет никакой нужды ходить на сервер.

Выкладываем их в одном месте, нашу HCMS в другом, и все.

Злоумышленник не имеет никакого способа узнать, где лежат наши данные/админка, а, значит, не знает откуда начинать ломать систему.

Готовые плагины, чаще всего, связаны с API их поставщиков (MailChimp, TypeForm, Stripe). Это серьезные дядьки, взломать которых является задачей не для мамкиных хакеров, а для скандинавских пиратов, которым такие взломы уже не интересны.

Итоги безопасности:

WordPress: -10 взлом сайта на WP это только вопрос времени. Садбатру.

SSG + HCMS: +10 злоумышленник не может узнать, где лежит ваша админка. Плагины сделаны на API серьезных фирм, которые гарантируют дополнительную безопасность.

P.S. Безусловно, никто не защитит от социальной инженерии. Но это вопрос ваших личных практик соблюдения безопасности в сети.

X. Контент не только для сайта


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

HCMS это API-first системы. Значит, ВЕСЬ их функционал полностью заточен, чтобы быть доступным через API (хоть HTTP, хоть GraphQL). Ergo, плагины, которые под них делают, тоже будут заточены под API.

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

image

У WordPress есть плагины, которые позволяют раздавать API через HTTP и GraphQL.

Но, вы понимаете насколько большая разница качества и удобства подобного функционала? Когда это не дополнительная функция с плагином, а основной механизм технологии?

Итоги по доступности контента:

WordPress: 0 баллов REST API рабочий, это факт. Но иногда приходится нехило извернуться при работе с ним, поэтому я не накинул балл. Я суров и полон предубеждений.

SSG + HCMS: +1 балл удобный и полнофункциональный API.

Вместо десерта


XI. Условная бесплатность


Поскольку ваш итоговый сайт представляет собой набор HTML, вы можете использовать GitHub, GitLab, Vercel, Netlify как бесплатный хостинг для своего сайта.

И даже когда вам понадобиться что-то мощнее, подобного рода хостинги стоят копейки.

Итоги хостинга:

WordPress: 0 баллов хостинг будет дешевый, но он всегда будет стоить денег.

SSG + HCMS: +2 балла за возможность хостить сайты абсолютно бесплатно и за минимальную стоимость хостинга при максимальной скорости.

XII. Это еще не все


Самое главное во всех CMS это текстовый редактор, который мы используем для создания контента.

WordPress Gutenberg на мой взгляд замечательный редактор (post-ironic mode off). Он удобнее, чем у Medium и чуть хуже редактора Notion. Здесь кастомизация и общее количество возможностей на высоком уровне, поэтому я снимаю шляпу и пальто.

Но SSG + HCMS не отстают:

Во-первых, если использовать HCMS, то вы получите очень мощные и красивые редакторы, схожие с Gutenberg.

Во-вторых, если вы можете писать контент в виде файлов (например, Markdown), а значит можете использовать хоть Linux терминал.

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

А в-четвертых, есть такие гибридные, крутые и специфичные инструменты, как TinaCMS, которая превращает статические страницы в Site builder.

И подобных решений для SSG море. Это позволяет выбрать любой из интересующих вас подходов.

Итоги по редактору:

WordPress: +1 балл правда хороший редактор.

SSG + HCMS: +2 балла 1 балл за редакторы, 1 балл за гибкость и возможности настройки.

XIII. Известность


SSG и HCMS это уже зрелые, известные и широкоиспользуемые в узкой среде решения.

Они существуют давно и никуда не денутся.

WordPress: +10 баллов тут без вопросов, WordPress известен гораздо сильнее и обыгрывает SSG + HCMS.

SSG + HCMS: +3 балла вундервафля для smooth operators, эту связку уже спокойно можно использовать в production + развитая экосистема + есть разработчики.

XIV. WordPress + SSG


Я это уже говорил выше, но повторюсь: вы можете использовать WordPress вместе с SSG!

То есть, вы пишите контент на WordPress и выгружаете его через отдельный SSG.

Есть даже возможность выгрузить статические HTML, без дополнительного SSG, используя специальный плагин! Например, этот (там еще прекрасное англоязычное видео о том же, о чем я сейчас говорю здесь).

Но, в этом случае, нужно учитывать, что другие плагины могут с ним не работать (например WooCommerce) и по-прежнему вы не властны над своим бандлом.

XV. Большие итоги


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

Лучше так: SSG + HCMS убийца WordPress?

Конечно, нет.

image

Если у вас среднего размера СМИ или у вас уже есть PHP разработчики, то вам подойдет WordPress.

Вы хотите сделать небольшой, но функциональный интернет-магазин вместе с удобным блогом за минимальную плату? вам нужен WordPress.

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

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

image

Широта возможностей WordPress впечатляет. Но даже такой универсальный инструмент подойдет не всем.

Это как демократия нравится всем, но у некоторых декадентов она вызывает только жалость об упущенных возможностях.

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

Время от времени я и сам выбираю WordPress, когда ситуация требует этого.

Но сейчас я делаю блог для себя, строю личный бренд, mea culpa. Поэтому мне важны:

  1. Полная кастомизация дизайна.
  2. Скорость отдачи контента.
  3. Свободное добавление кастомного функционала.
  4. Безопасность.
  5. Простота обсуживания и деплоя.
  6. Меня достало копаться в css и ставить везде !important.
  7. Меня достало возиться с полурабочими плагинами.
  8. Я не хочу бэкапить БД зная, что любой апдейт, плагин, да что угодно, могут развалить весь мой сайт.
  9. Я хочу использовать современные технологии и современную экосистему для удобной разработки.
  10. При этом я Full-stack, поэтому для меня нет никакой проблемы в самостоятельной кастомизации системы.


И все это из коробки или с возможностью написать с нуля, а не бороться с CSS, JS, PHP и плагинами.

Дисклеймер в конце, как вы любите


Поэтому я выбираю SSG в его Gatsby.js инкарнации + Headless CMS на Ghost.io.

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

(Отойдите от экранов, ща будет революционный лозунг!)

Пора сделать Pull Request к чему-то новому! Плагины для всех CMS, объединяйтесь!

Да, может сейчас количество плагинов для SSG + HCMS несравнимо с кучей шлака под WordPress (особенно, для русскоязычного сегмента), но почему бы не начать их пилить?

Шаг за шагом вкладываясь в экосистему SSG этот инструмент имеет все шансы догнать WordPress.

Вот например на создание Headless FAQ хватило всего 3-х дней и день, чтобы подключить его в SSG блоге и даже на WordPress блоге (форма внизу страницы).

Короче, я хотел чего-то нового, оно меня нашло. Я его попробовал и теперь новое решение меня полностью устраивает.

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

Круто, если вы дочитали до конца, спасибо.

Не могу не добавить стандартное окончание:

Был ли у вас опыт работы с SSG + HCMS? Что понравилось, а что оставило неприятное послевкусие?

Серебряных пуль не бывает. Бывают проблемы, которые вы готовы или не готовы проглотить.
Подробнее..
Категории: Cms , Wordpress , Headless-cms , Gatsbyjs , Ghostio , Jam , Static site , Ssg

SEO-плагины пишутся шарлатанами?

25.02.2021 16:13:17 | Автор: admin

Или лучше "Значительная часть SEO-плагинов под WordPress пишется шарлатанами?", если формулировать вопрос с доскональной точностью.

По ряду причин в последнее время я занимаюсь перебором SEO-плагинов на своем собственном сайте. Включаю то один, то другой от флагманов к малоизвестным. Смотрю, что как работает. И вот что увидела.

1. Ключевые запросы

Главное, что бросилось в глаза, это работа плагинов с классическими SEO-ключами. Ходят слухи, что те НЕ нужны ни Яндексу, ни Гуглу в последние несколько лет. Что поисковые системы успешно научились распознавать синонимы и вообще отлично улавливают общий смысл текста.

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

Косвенным подтверждением этой теории выступает тот факт, что в новейших стандартных темах Вордпресс строка keywords в принципе отсутствует в блоке header. Это несложно увидеть в исходном коде. Причем и плагины, о которых я сейчас пишу, такую строку никуда не добавляют. Однако продолжают требовать ключи в своих вебмастерских внутренних интерфейсах.

Если не заполнять соответствующие графы или заполнить их не точными словами-фразами, то начинают ругаться и выдавать советы типа "этот ваш материал надо бы улучшить", "вы в красной зоне, а не в зеленой", "страница нормальная всего на 58%", "в тексте не обнаружено", "плотность не та"...

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

Они фактически вводят пользователей в заблуждение. Спасибо, если приписывают в каком-нибудь неприметном месте, что эти рекомендации по запросам, мол, наша собственная внутренняя оценка. Но ведь все эти грозные фразы (двумя абзацами выше) явно сигнализируют иное: "вам светят проблемы с поисковиками"; "оптимизация никуда не годится". А сигнал ведь ложный.

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

Плюс есть момент с платными версиями. Покупайте, мол, наши платные пакеты и работайте с большим количеством ключевых запросов (помимо другого дополнительного функционала). Однако что значит "работайте"? В контексте происходящего?.. Количество ключей увеличится, окей а качество анализа и рекомендаций?

1.1. Качество контента

Это уже сто лет известно, но все-таки продолжает раздражать. Странные алгоритмы оценки статей. Длина абзацев еще ладно. Но "читаемость"? "Водность"? "Бессмысленность и тленность"?

Условный Главред или Орфограммка потому и прославились, что не каждый суслик агроном. В смысле, не так уж здорово машинам удается оценивать тексты, нормальный сервис надо еще поискать. И даже обнаруженному нормальному не стоит верить огульно. Но нет, плагинщики "я сделялъ" настаивают.

Фе.

2. Мета-описания description

Наблюдение номер два: разработчики любят включать в пакеты такой функционал, как автоматическую генерацию мета-описаний. Реализуется это как вырезание кусков текста с вашей веб-страницы.

Но ведь выбирать цитаты для сниппета умеют и Яндекс, и Гугл. Причем делают это на вполне приличном уровне. Можно усомниться в том, что они возьмут наиболее удачный кусок статьи в качестве цитаты. Однако откровенного мусора, как минимум, не насыплют.

А некоторые плагины именно насыпают мусор например, принудительно втыкают в начало "автоматически сгенерированного description" метки и рубрики записи. В самых тяжелых случаях не разделяя их ни пробелами, ни знаками (*да, правда было). Причем далеко не всегда предусмотрена техническая возможность настраивать "состав автосниппета" или достаточно гибко корректировать процесс вручную.

То есть поисковики очевидно справятся с этой работой лучше. Если не лезть.

А разработчики плагинов, простите, имитируют_функциональность.

3. И полное игнорирование моего мнения

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

  1. Мне тишком хлопали noindex на все страницы пагинации. Скромно замечу, что пагинация не обязательно делается автоматически в самом Вордпрессе, в Гутенберге, в числе базовых блоков есть "разрыв страницы".

    А у меня есть статья на 3000+ слов, которую я посчитала уместным разбить на несколько страниц потому что мне так видится логичным. И да, я прекрасно знаю, что листать поперек удобно на мониторах и не удобно на телефонах; на телефонах сподручнее прокручивать вниз. Но это мое дело, n'est-ce pas?

    Логика работы плагина явно противоречит логике работы CMS.

  2. Мне переписывали роботс. Причем спрашивали, "вам сохранить существующий роботс, скопировать все оттуда?" а положительный ответ сначала принимали, а потом... Потом все-таки пытались вываливать в выдачу пачку легендарных мусорных страниц wp-json. Потому что запрещающая строка в итоге куда-то самопроизвольно пропадала из файла.

  3. Карту сайта тоже переписывали кто во что горазд. Например, умудрялись поместить в нее ссылку на удаленный черновик записи. WTF?

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

Ни_разу никто не вывел сообщение-предупреждение "такие-то настройки я сейчас изменю". Какого черта?

***

Предвижу советы про наем профессионала. Который все отладит как мне надо, в крайнем случае вставит-сотрет куски кода и вообще... Разрешите возразить.

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

Обратите внимание, как плагины позиционируются и рекламируются. Их представляют "инструментами для простой удобной настройки" и бла-бла-бла. А фактически заявленный функционал не обеспечивается. Зачастую имеет место имитация в комплекте с искусственным усложнением жизни вебмастера, чтобы тот видел активность (и оставался в тонусе). Спасибо.

унылый_котик.жпг

Такие дела.

Подробнее..

Recovery mode Проблемы монетизации продуктов на WordPress. Часть 2

06.10.2020 12:23:44 | Автор: admin
В первой части этой статьи мы обсудили технические трудности, которые ожидают тех, кто решит монетизировать свои продукты для WordPress, сегодня, продолжим разбираться c нетехническими, но не менее важными проблемами.

Аналитика и отслеживание использования
WordPress.org потрясающий репозиторий и один из ключевых компонентов, которые делают WordPress успешным наличие плагина в выдаче это здорово, но данные, которые вы можете передать пользователю минимальны.

image

Все, что вы можете донести до пользователя, это количество загрузок и приблизительное количество активных установок, и все Кто, что, где, как и почему? Это означает, что большинство решений, принимаемых пользователями, будут основываться на интуиции и предположениях, а не на данных.
Более того, политика WordPress.org направлена против любого автоматического отслеживания любых действий пользователей продуктов. Если вы прочитаете официальные инструкции, в них четко указано, что разработчикам не разрешается звонить домой без разрешения пользователя. Таким образом, отслеживать любое использование вашего плагина или темы сложно для этого требуется явное согласие владельца сайта. Итак, подавляющее большинство авторов плагинов и тем летают вслепую, полагаясь в одиночку на свое чутье и предвзятые отзывы пользователей, что во многих случаях мешает им продвигать и развивать свои собственные продукты.
Некоторые разработчики практически наверняка не знают, как пользователи используют из продукт, чего им не хватает, чем довольны и напротив, чем раздражены... поэтому, как вы можете себе представить, некоторые продукты WordPress страдают от очень плохого UX, что создает трудности конечному пользователю. Фактически, 20 процентов удалений плагинов связаны с плохим FTUX (первым пользовательским опытом). Пользователь устанавливает плагин и не понимает, что делать дальше.
Это довольно удивительно, потому что каждый 4-й веб-сайт это WordPress, в среднем на каждом веб-сайте WordPress работает 17,6 активных плагинов и одна тема, так что буквально половину всех веб-сайтов обслуживают разработчики, которые работают в темноте, потому что у них нет данные об использовании их программного обеспечения.

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

Основные (нетехнические) проблемы при монетизации продуктов WordPress


Маркетинг
Представим на секунду, что ваш бесплатный плагин или тема WordPress установлены и активированы на 1 000 000 веб-сайтов. Вы понятия не имеете, кто этим пользуется. В идеальном мире вы можете написать им прямо по электронной почте о ваших крутых новых функциях в предложении Pro. Но, к сожалению, у вас нет их электронной почты или другого прямого канала связи.
Так как же привлечь трафик на страницу оформления покупки?
Именно здесь должны проявиться ваши маркетинговые навыки. Самый эффективный способ использовать ваши 1000000 существующих веб-сайтов это добавить маркетинговые материалы в настройки плагина на панели администратора.
Приготовьтесь к волне негатива, репозиторий Wordpress очень консервативная экосистема. Негодование пользователей может вызвать безобидная реклама в админке, информационный нотис и даже просто, цвет в интерфейсе на пару тонов отличающийся от дашборда WordPress.
К сожалению, вы не сможете связаться со всеми владельцами сайта. Только пользователи, которые обновятся до вашей последней версии программного обеспечения и перейдут на страницу настроек, где вы добавили свое маркетинговое предложение, узнают о нем. Чтобы расширить это, вы, вероятно, захотите создать свой собственный веб-сайт, написать контент для SEO, попытаться привлечь потенциальных клиентов с помощью поиска, провести входящий маркетинг в социальных сетях и т. Д.
Покупатели вряд ли просто появятся и постучат в вашу дверь. Если у вас нет опыта в маркетинге, вам нужно прочитать МНОГО статей и провести много тестов. Не поймите меня неправильно, это выполнимо, и это не ракетостроение просто больше навыков, которые вам нужно изучить и с которыми нужно справиться.

Служба поддержки
Запуск бесплатного стороннего проекта это весело. Никаких обязательств перед кем-либо, вы можете отказаться от проекта, когда захотите, и, как правило, пользователи не должны возлагать большие надежды на поддержку. Программное обеспечение типа Используйте это на свой страх и риск.
Но когда вы начинаете бизнес по разработке плагинов для WordPress, это совсем другая история. Даже если вы напишите заглавными буквами, что ПОДДЕРЖКИ НЕТ, клиенты будут ожидать, что вы поможете им и решите проблемы, с которыми они сталкиваются.
Это зависит от каждого конкретного продукта, но вы очень скоро поймете, что до 50 процентов вашего времени посвящается поддержке. Не для разработки крутых функций, как вы могли представить, впервые отправившись в это путешествие. Обращаться с клиентами может быть сложно, особенно когда вы не в лучшем настроении. Кроме того, если вы работаете в одиночку, это означает, что если вы загораете на пляже в Турции во время ежегодного отпуска, вам все равно нужно быть начеку и принимать заявки на поддержку почти в реальном времени. Вы не можете отправлять своим клиентам по электронной почте: Привет, ребята, у меня двухнедельный отпуск в Турции, так что удачи и надеюсь, что не будет никаких проблем! .
Хорошая поддержка очень важна для вашего бренда. Если вы окажете неадекватную поддержку, клиенты перестанут использовать ваш продукт и начнут говорить о ваших услугах на публичных форумах. Более того, очень быстро средняя оценка в 4,7 звезды, над которой вы так усердно работали в течение последних 12 месяцев, упадет до 3,2, даже если ваша поддержка связана только с PRO плагином. Почему? Потому что пользователям плевать, что оценки на WordPress.org должны относиться только к перечисленным там бесплатным плагинам.
Создать плагин WordPress, за который люди готовы платить, непросто. Вы определенно можете получить минимальный доход, просто создав Pro версию своего бесплатного плагина. Но если вы действительно хотите построить на этом бизнес, вам нужно обладать множеством навыков что, скорее всего, означает создание команды. Кодирование это всего лишь часть. Я бы сказал: код 2/8, поддержка 3/8 и маркетинг 3/8. Если вы уделяете слишком много внимания одной из этих областей, страдают другие. Найти этот баланс непросто, требуется время и постоянный мониторинг.

Наша команда прошла этот путь, набив по дороге необходимое количество шишек. Из 20 созданных нами плагинов, 11 имеют Pro версии и продаются.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru