У нас полно лендингов и бизнес просит еще! Но каждый новый нужно проводить по тому же процессу. Хочется автоматизации — нужен конструктор! Я много раз встречал статьи на Хабре про самописные решения — 5 лет, и все в лучшем виде, но для нас это «too much». Мы уложились менее чем за год с «Bitrix». До этого прошерстили рынок на предмет типовых решений, а также отбросили вариант решения с нуля. Расскажу о процессе выбора и что мы в итоге получили.
Как возникла идея
За свою карьеру я создал бесчисленное количество лендингов. Я делал это в маленьких веб‑студиях, затем я делал это в крупных enterprise компаниях.
В мини студии нам достаточно было сверстать лендинг и натянуть на готовую CMS (чаще всего это был какой‑нибудь Wordpress). А затем залить это все на хостинг и можно было отдавать заказчику.
В крупной компании, создавая типовой сайт, мы должны соблюдать порядки деплоя и релиз‑менеджмента, тестирования (включая пентесты ИБ), аналитики и т. д., что соответственно повышает трудозатраты.
Но и тут есть что‑то общее — повторяемость этапов разработки от лендинга к лендингу, независимо от сложности проекта и величины компании.
И вот, когда нам в очередной раз прилетает запрос на новый лендинг, мы решаем что пора что‑то с этим делать.
Я отправился на поиски. Но с чего начать? Какие задачи мы хотим для себя решить?
Какие задачи должны быть решены
Так как это наш внутренний проект — мы сами сформировали требования:
увеличение скорости разработки/создания типовых страниц за счет готовых блоков редактора
снижение трудозатрат на создание типовых решений, т.к. наполнение должно перекладывается на контент‑менеджера или другое ответственное лицо
это должна быть централизованная система — не потребуется выделение новой инфраструктуры, закупки лицензий, формирования команды для реализации задачи и т. д.
предоставить достаточно гибкую ролевую модель для управления сайтами и их коллекциями — это позволит нам назначать ответственных и делегировать им права
система должна быть расширяемая, с точки зрения функциональных блоков редактора
предоставлять возможность выбора и заведения субдоменов за минимальное число шагов
исключить из процесса разработки максимум этапов и согласований, путем автоматизации (рассылка писем с согласованиями ролей, запросов к ИБ, статусами и т. д.)
Но в первую очередь нас все‑таки интересовал редактор страниц, чтобы он был подобием «Tilda». Так может напишем свое решение?
Почему не стали писать решение с нуля
Сначала я действительно планировал начать проектировать новое решение, разрабатывать свой редактор, используя VueJs или React (потратил как минимум пару дней, ставил дополнения, редакторы и т. д.). Но я понимал, что, скорее всего, эту задачу уже решали и незачем изобретать велосипед.
Рассматривал платформы на основе CMS Wordpress и его плагинов (которых выпущено огромное количество, а на изучение каждого понадобилось бы очень много времени), а также некоторые другие CMS и библиотеки.
Пробежался по разным статьям (включая Хабр) и на основе опыта других (особенно их сроков реализации) решил отбросить данные варианты, так как ищем что‑то простое, а нужно нам это уже «вчера».
Затем я вспоминаю, как мне доводилось работать с редактором на основе решения «Сайты24» от «Bitrix», который уж очень был похож на «Tilda». В связи с имеющимся опытом, решил на нем остановится подробнее.
Как устроен редактор
Интерфейс визуального редактора действительно похож на "Tilda":
Блоки редактора состоят из html шаблона, с разметкой на области:
<?php /* пример файла block.php */ ?>
<section class="landing-block container-fluid px-0">
<div class="row no-gutters g-overflow-hidden landing-block-inner">
<div class="landing-block-card col-lg-6 landing-block-node-img g-min-height-500 g-bg-black-opacity-0_6--after g-bg-img-hero row no-gutters align-items-center justify-content-center u-bg-overlay g-transition--ease-in g-transition-0_2 g-transform-scale-1_03--hover js-animation fadeInUp"
style="background-image: url(https://cdn.bitrix24.site/bitrix/images/landing/business/900x506/img1.jpg);">
<div class="text-center u-bg-overlay__inner">
<h5 class="landing-block-node-title landing-semantic-subtitle-image-medium js-animation animation-none text-uppercase g-font-weight-700 g-color-white g-px-40 g-pt-40 g-mb-20">
Building</h5>
<div class="landing-block-node-text landing-semantic-text-image-medium js-animation animation-none g-color-white-opacity-0_7 g-px-40">
<p>Sed feugiat porttitor nunc, non dignissim ipsum vestibulum in. Donec in blandit dolor.
Vivamus a fringilla lorem, vel faucibus ante.</p>
</div>
<div class="landing-block-node-button-container g-px-10 g-pb-40">
<a class="landing-block-node-button landing-semantic-link-image-medium js-animation animation-none btn g-btn-type-outline g-btn-white g-btn-px-m rounded-0 g-btn-size-md mx-2 g-color-white g-color-black--hover"
href="#">
Read more
</a>
</div>
</div>
</div>
<div class="landing-block-card col-lg-6 landing-block-node-img g-min-height-500 g-bg-black-opacity-0_6--after g-bg-img-hero row no-gutters align-items-center justify-content-center u-bg-overlay g-transition--ease-in g-transition-0_2 g-transform-scale-1_03--hover js-animation fadeInUp"
style="background-image: url(https://cdn.bitrix24.site/bitrix/images/landing/business/900x506/img2.jpg);">
<div class="text-center u-bg-overlay__inner">
<h5 class="landing-block-node-title landing-semantic-subtitle-image-medium js-animation animation-none text-uppercase g-font-weight-700 g-color-white g-px-40 g-pt-40 g-mb-20">
Plumbing works</h5>
<div class="landing-block-node-text landing-semantic-text-image-medium js-animation animation-none g-color-white-opacity-0_7 g-px-40">
<p>Sed feugiat porttitor nunc, non dignissim ipsum vestibulum in. Donec in blandit dolor.
Vivamus a fringilla lorem, vel faucibus ante.</p>
</div>
<div class="landing-block-node-button-container g-px-10 g-pb-40">
<a class="landing-block-node-button landing-semantic-link-image-medium js-animation animation-none btn g-btn-type-outline g-btn-white g-btn-px-m rounded-0 g-btn-size-md mx-2 g-color-white g-color-black--hover"
href="#">
Read more
</a>
</div>
</div>
</div>
</div>
</section>
Области описываются в его манифесте, чтобы редактор понимал, чем можно управлять:
<?php //пример файла descriptions.php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) {
die();
}
use \Bitrix\Main\Localization\Loc;
return [
'block' => [
'name' => Loc::getMessage('LANDING_BLOCK_7_FOUR_COLS_BIG_BGIMG_TITLE_TEXT_BUTTON_NAME'),
'section' => ['tiles', 'news'],
],
'cards' => [
'.landing-block-card' => [
'name' => Loc::getMessage('LANDING_BLOCK_7_FOUR_COLS_BIG_BGIMG_TITLE_TEXT_BUTTON_NODES_LANDINGBLOCK_CARD'),
'label' => ['.landing-block-node-title'],
],
],
'nodes' => [
'.landing-block-node-img' => [
'name' => Loc::getMessage('LANDING_BLOCK_7_FOUR_COLS_BIG_BGIMG_TITLE_TEXT_BUTTON_NODES_LANDINGBLOCKNODEIMG'),
'type' => 'img',
'dimensions' => ['width' => 960],
'create2xByDefault' => false,
],
'.landing-block-node-title' => [
'name' => Loc::getMessage('LANDING_BLOCK_7_FOUR_COLS_BIG_BGIMG_TITLE_TEXT_BUTTON_NODES_LANDINGBLOCKNODETITLE'),
'type' => 'text',
],
'.landing-block-node-text' => [
'name' => Loc::getMessage('LANDING_BLOCK_7_FOUR_COLS_BIG_BGIMG_TITLE_TEXT_BUTTON_NODES_LANDINGBLOCKNODETEXT'),
'type' => 'text',
],
'.landing-block-node-button' => [
'name' => Loc::getMessage('LANDING_BLOCK_7_FOUR_COLS_BIG_BGIMG_TITLE_TEXT_BUTTON_NODES_LANDINGBLOCKNODEBUTTON'),
'type' => 'link',
],
],
'style' => [
'block' => [
'type' => ['block-default-wo-background'],
],
'nodes' => [
'.landing-block-card' => [
'name' => Loc::getMessage('LANDING_BLOCK_7_FOUR_COLS_BIG_BGIMG_TITLE_TEXT_BUTTON_NODES_LANDINGBLOCK_CARD'),
'type' => ['columns', 'background-overlay', 'animation'],
],
'.landing-block-node-title' => [
'name' => Loc::getMessage('LANDING_BLOCK_7_FOUR_COLS_BIG_BGIMG_TITLE_TEXT_BUTTON_STYLE_LANDINGBLOCKNODETITLE'),
'type' => ['typo', 'animation', 'heading'],
],
'.landing-block-node-text' => [
'name' => Loc::getMessage('LANDING_BLOCK_7_FOUR_COLS_BIG_BGIMG_TITLE_TEXT_BUTTON_STYLE_LANDINGBLOCKNODETEXT'),
'type' => ['typo', 'animation'],
],
'.landing-block-node-button' => [
'name' => Loc::getMessage('LANDING_BLOCK_7_FOUR_COLS_BIG_BGIMG_TITLE_TEXT_BUTTON_NODES_LANDINGBLOCKNODEBUTTON'),
'type' => ['button', 'animation'],
],
'.landing-block-node-button-container' => [
'name' => Loc::getMessage('LANDING_BLOCK_7_FOUR_COLS_BIG_BGIMG_TITLE_TEXT_BUTTON_NODES_LANDINGBLOCKNODEBUTTON'),
'type' => 'text-align',
],
'.landing-block-inner' => [
'name' => Loc::getMessage('LANDING_BLOCK_7_FOUR_COLS_BIG_BGIMG_TITLE_TEXT_BUTTON_NODES_LANDINGBLOCK_INNER'),
'type' => 'row-align',
],
],
],
];
Информация по созданию блоков подробно описана в документации и сейчас рассматриваться не будет. На самом деле и без документации относительно легко разобраться в зависимостях между разметкой в блоках и файле манифеста на основе имеющихся блоков из коробки.
Добавленные блоки редактора сохраняются в БД в виде html.
При этом, такой блок может содержать в себе полноценный «Bitrix» компонент с динамическими параметрами, которые можно вынести в настройки блока визуального редактора, а поддержка компонентов — это возможность разработки уникального функционала под свои задачи.
Но не все, что предоставляется из коробки нам подошло.
Особенности, на которые стоит обратить внимание
Функционал "Сайты24" из коробки позволяет публиковать страницы при наличии лицензии только с активным сроком обновлений. Но для нас наличие активной лицензии и так обязательно. Поэтому данное ограничение мы посчитали незначительным.
Нас интересовал удобный публичный интерфейс для работы с сайтами. Если в «Битрикс24» определенный интерфейс есть, то в «БУС» — вся работа с сайтами выполняется через админ панель.
Неудобством для нас стало и то, что для работы с веб‑формами сайтов необходима интеграция с «CRM». То есть «БУС» надо связывать с «Битрикс24», а мы хотели бы иметь минимальное количество зависимостей между системами.
Так же существует разница в работе самого решения «Сайты24» на «БУС» и «Битрикс24», которые тоже стоит учитывать. Например, типы сайтов: для «БУС» из коробки используют тип «SMN» — при этом лендинг конструктора обязательно должен быть привязан к физическому сайту системы битрикс. Это порождает проблемы роутинга, так как при публикации лендинга создается новая директория, вместо общего контроллера. А еще нужно учитывать хранение этих директорий под Git. Количество типов ограничено и на данный момент их расширение не предусмотрено системой. Подробности тут.
Следующий нюанс — по умолчанию система на «Bitrix24» публикует сайты в облаке, а на платформе БУС — локально. Для перевода публикации сайтов на платформе «Bitrix24»(коробка) на локальную установку требуется использовать заглушку.
Потребовалось вырезать много лишнего для нас функционала, не нарушив основную работу редактора и системы. Но несмотря на найденные для нас недостатки, делаем вывод, что редактор нам подходит. Сопровождение решения не вызовет больших трудозатрат, так как решение «Сайты24» сопровождает «Bitrix», а не мы. Остановились на редакции БУС — «стандарт» (но и редакция «старт» тоже подходит)
Чтобы быстро все это разворачивать — напишем свой модуль, вырежем из коробки все что нам не понадобилось, попросим DevOps коллег организовать нам Playbook с шагами авторазворота всей системы.
Дорабатываем решение под себя
Интерфейс администрирования разработан на основе «Битрикс24». Оставлен функционал создания разделов, страниц, сам редактор и некоторые его настройки.
Упростил форму добавления нового сайта — теперь необходимо указывать только домен + название сайта. Остальные параметры автоматически заполняются, но их всегда можно поменять в расширенном режиме (для определенных ролей).
Из редактора были отключены все блоки требующие интеграции с «CRM», различные формы и интеграции с другими системами, техподдержка и реклама. Добавлены свои веб‑формы (о которых расскажу подробнее), а так же вырезаны другие мелочи, на которых не буду заострять внимание.
Добавлен общий контроллер для отображения сайтов относительно домена и прав пользователя.
Для нашего удобства мы группируем ответственность за сайты. Для этого были реализованы «коллекции». Некая обертка над стандартными сайтами. (Из коробки ролевая модель отдает предпочтение работе с одним конкретным сайтом).
Так у нас помимо ролей "Администратор" и "Контент-менеджер", появились две новые роли:
«Технический владелец» — отвечает за назначение и согласование ролей в рамках его коллекции, а так же за заведение сайтов и доменов.
«Бизнес владелец» — отвечает за коллекцию сайтов и согласовывает все изменения, поступающие от других пользователей, включая подтверждение ролей.
Коллекции
Теперь давайте подробнее рассмотрим функционал коллекций.
Принцип работы следующий: администратор системы создает коллекцию и назначает ответственных (бизнес и технические владельцы), им отправляется запрос на почту с запросом на подтверждение их ролей. Пока подтверждение не будет получено от обоих владельцев - с коллекцией нельзя будет работать.
Как только роли назначены и подтверждены - владельцы открывают доступ к этой коллекции для контент менеджеров, создают сайты и заводят домены.
Теперь рассмотрим пример реализации коллекций: (в примерах ниже умышленно приведен не законченный код, чтобы показать только один из возможных вариантов реализации)
В рамках контекста пользователь имеет роль, которая обладает набором прав.
<?php
namespace NLMK\Constructor\Collection\Context\Contracts;
use NLMK\Constructor\Collection\Role\Contracts\AbstractRole;
/**
* Interface Context
* @package NLMK\Constructor\Collection\Context\Contracts
*/
interface Context
{
/**
* @param AbstractRole $role
* @param int $userID
* @return Context
*/
public static function create(AbstractRole $role, int $userID): Context;
/**
* @return bool
*/
public function canOpenCollection(): bool;
/**
* @return bool
*/
public function canCreateCollection(): bool;
/**
* @return bool
*/
public function canEditCollection(): bool;
/**
* @return bool
*/
public function canDeleteCollection(): bool;
/**
* @return bool
*/
public function canViewPanel(): bool;
}
Например, он может иметь права на просмотр определенной коллекции - метод «canOpenCollection», если его роль присутствует в списке:
<?php
namespace NLMK\Constructor\Collection\Context;
use NLMK\Constructor\Collection\Context\Contracts\AbstractContext;
use NLMK\Constructor\Collection\Role\Business;
use NLMK\Constructor\Collection\Role\ContentManager;
use NLMK\Constructor\Collection\Role\Contracts\AbstractRole;
use NLMK\Constructor\Collection\Role\Technical;
/**
* Class Context
* @package NLMK\Constructor\Collection\Context
*/
final class Context extends AbstractContext
{
/**
* @param int $userID
* @param AbstractRole $role
* @return static
*/
public static function create(AbstractRole $role, int $userID): self
{
...
}
/**
* @return bool
*/
public function canOpenCollection(): bool
{
return $this->isAdminContext()
|| $this->role instanceof Technical
|| $this->role instanceof Business
|| $this->role instanceof ContentManager;
}
/**
* @return bool
*/
public function canEditCollection(): bool
{
...
}
/**
* @return bool
*/
public function canCreateCollection(): bool
{
...
}
/**
* @return bool
*/
public function canDeleteCollection(): bool
{
...
}
/**
* @return bool
*/
public function canViewPanel(): bool
{
...
}
}
В рамках этого метода мы указываем, какие роли могут просматривать коллекцию. Соответственно, чтобы расширить ролевую модель - добавляем новую строку в метод и новую роль.
В реализации роли достаточно указать символьный код пользовательского поля, в рамках которого происходит привязка пользователя к этой роли:
<?php
namespace NLMK\Constructor\Collection\Role;
use NLMK\Constructor\Collection\Role\Contracts\AbstractRole;
/**
* Class ContentManager
* @package NLMK\Constructor\Collection\Role
*/
final class ContentManager extends AbstractRole
{
/**
* @var string
*/
protected static string $code = 'UF_MANAGER';
/**
* @return AbstractRole
*/
public static function createRole(): AbstractRole
{
return new self(self::$code);
}
}
В директории «services» описываем «Use Cases» сценарии. Подтверждение роли, создание заявки на подтверждение роли, делегирование прав и отправка оповещений.
<?php
namespace NLMK\Constructor\Collection\Services\Contracts;
use Throwable;
/**
* Interface Services
* @package NLMK\Constructor\Collection\Services\Contracts
*/
interface Services
{
/**
* @param array $params
* @return bool
* @throws Throwable
*/
public static function action(array $params): bool;
}
Рассмотрим пример реализации сценария отправки уведомлений по email:
<?php
namespace NLMK\Constructor\Collection\Services;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Mail\Event;
use CEventMessage;
use CSite;
use NLMK\Constructor\Collection\Services\Contracts\Services;
use Throwable;
/**
* Class EmailMessage
* @package NLMK\Constructor\Collection\Services
*/
final class EmailMessage implements Services
{
/**
* Отправляет email сообщение из кастомного шаблона
* @param array $params
* @return bool
* @throws Throwable
*/
public static function action(array $params): bool
{
return self::sendEmailNotify($params['SUBJECT'], $params['EMAIL_TO'], $params['MESSAGE']);
}
/**
* @param string $subject
* @param string $email
* @param string $message
* @return bool
*/
private static function sendEmailNotify(string $subject, string $email, string $message): bool
{
$event = (new CEventMessage)->GetList(false, false, [
'EVENT_NAME' => 'NLMK_CUSTOM_EVENT_TEMPLATE',
'LID' => CSite::GetDefSite()
])->Fetch();
return Event::send([
'EVENT_NAME' => $event['EVENT_NAME'],
'LID' => $event['LID'],
'C_FIELDS' => [
'EMAIL_TO' => $email,
'MESSAGE' => $message,
'SUBJECT' => $subject
]
])->isSuccess();
}
}
Такая минимальная структура позволяет покрыть основные потребности управления и делегирования прав доступа в рамках коллекции. А на сопровождение нужны минимальные трудозатраты.
Веб-формы
Чтобы исключить зависимость «БУС» от «CRM» — решил реализовать свои веб‑формы. Наибольшую популярность имеет практика реализации форм на основе «инфоблоков» (например решение от Аспро). Когда инфоблок — это веб‑форма, а свойства инфоблока — это поля. Интерфейс создания форм интегрировал в редактор.
Для полного замещения веб‑форм «CRM» понадобилось 4 новых компонента:
form.edit — создание\изменение формы
form.field.edit — создание\изменение полей формы
form.list — работа со списком веб‑форм
form.view — отображение веб‑формы на странице
Добавляем блок веб‑формы на страницу:
После добавления блока в редактор и клика по кнопке «+веб форма» — открывается интерфейс редактирования форм:
Где на VueJS реализовал компонент редактирования полей:
(пункт меню «изменить поля формы»)
После добавления полей и применения веб-формы (пункт меню "использовать веб-форму"), блок динамически подтягивает компонент:
На опубликованной странице веб-форма имеет тот же вид, что и в редакторе. Присутствует минимальная валидация полей и вывод ошибок. Блок формы можно модифицировать по кнопке "Дизайн", где можно поменять практически все ее элементы.
Видимость только для внутренних пользователей
У нас часто возникает необходимость ограничения доступа к сайтам, чтобы они были доступны только для внутренних сотрудников компании. В этом нам помогает интеграция с системой «Blitz».
Если кратко, «Blitz Identity Provider» — это решение, которое облегчает доступ к приложениям и обеспечивает плавное переключение между ними. Достаточно выполнить аутентификацию один раз и можно ходить по приложениям, имеющим интеграцию с ней (не выполняя повторную аутентификацию), пока не истечет токен или будет выполнен выход и т. д.
«Blitz» предоставляет разнообразные методы аутентификации, включая разные варианты двухфакторной аутентификации на основе «OAuth 2.0».
Многие наши приложения уже имеют интеграцию с этой системой. На основе нее мы можем группировать пользователей на внешних и внутренних.
Для интеграции с этой системой у нас есть свое решение (подробности реализации тут не будут описываться, так как статья не про это). Поэтому мы добавим модуль для интеграции с этой системой в нашу сборку.
В настройках нашего конструктора из коробки можно выставлять права доступа для сайтов, но нам нужно интегрировать настройку с «Blitz»:
Зарегистрируем в нашей системе новый «Access‑провайдер» на основе базового функционала провайдеров «Битрикс»:
<?php
namespace NLMK\Constructor\Access\Providers;
use Bitrix\Main\Event;
use Bitrix\Main\EventManager;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\UserAccessTable;
use CAuthProvider;
use CFinder;
use IProviderInterface;
class NlmkAccessProvider extends CAuthProvider implements IProviderInterface
{
const ID = 'nlmk_ap';
public static function OnAfterUserLogin($event)
{
if ($event instanceof Event) {
self::recalculateBlitzAccess((int)$event->getParameter('ID'));
}
}
/**
* Метод производит рекалькуляцию прав провайдера nlmk_ap
*/
private static function recalculateBlitzAccess(int $userID): void
{
$provider = new self;
$self->RemoveCode($userID, 'NLMK_AP');
UserAccessTable::add([
'USER_ID' => $userID,
'PROVIDER_ID' => self::ID,
'ACCESS_CODE' => 'NLMK_AP',
]);
}
/**
* Метод возвращаем форму с выбором провайдера
*/
public function GetFormHtml($arParams = false)
{
$elements = '';
$arFinderParams = [
'PROVIDER' => 'nlmk_ap',
'TYPE' => 3,
];
$arItem = [
'ID' => 'NLMK_AP',
'AVATAR' => '/bitrix/js/main/core/images/access/avatar-user-auth.png',
'NAME' => Loc::getMessage('MESSAGE_PROVIDER_NAME_NLMK_AP'),
'DESC' => Loc::getMessage('MESSAGE_PROVIDER_NAME_NLMK_AP'),
];
$elements .= CFinder::GetFinderItem($arFinderParams, $arItem);
$arPanels = [
[
'NAME' => Loc::getMessage('MESSAGE_NLMK_CUSTOM_ACCESS'),
'ELEMENTS' => $elements,
'SELECTED' => 'Y',
],
];
$html = CFinder::GetFinderAppearance($arFinderParams, $arPanels);
return ['HTML' => $html];
}
public function GetNames($arCodes)
{
return [
'NLMK_AP' => ['provider' => '', 'name' => Loc::getMessage('MESSAGE_PROVIDER_NAME_NLMK_AP')],
];
}
public static function GetProviders()
{
return [
[
'ID' => self::ID,
'CLASS' => self::class,
'SORT' => 1,
'NAME' => Loc::getMessage('MESSAGE_NLMK_CUSTOM_ACCESS'),
'PROVIDER_NAME' => '',
],
];
}
public static function registerEvents()
{
EventManager::getInstance()->addEventHandler(
'nlmk.blitzauth',
'OnAfterUserLogin',
[self::class, 'OnAfterUserLogin']
);
EventManager::getInstance()->addEventHandler(
'main',
'OnAuthProvidersBuildList',
[self::class, 'GetProviders']
);
}
}
После подключения нашего провайдера к системе появляется возможность устанавливать права для внутренних пользователей компании:
Итоги проделанной работы и выводы
На данный момент наш проект проходит финальное тестирование и уже вызывает огромный интерес различных функциональных направлений.
Синтетические измерения и тесты продемонстрировали следующие результаты: не погруженный в систему разработчик смог создать копию реального сайта всего за 5 часов, что является значительным улучшением, по сравнению с более чем 20 днями, относительно оригинала. Это свидетельствует о перспективах сокращения временных и ресурсных затрат при разработке типовых лендингов.
Делая погрешность на то, что с системой будут работать пользователи с техническими знаниями ниже чем у разработчика, то времени на создание первого сайта уйдет больше, но значительно меньше, чем при создании новой информационной системы.