Перейти к содержимому
VC
Кейс № 25 · Веб · Инструменты

Десктоп-генератор промо-лендингов: бриф или ссылка → готовая страница

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

Тип проекта
Собственный инструмент · внутренняя разработка
Стек
Python · tkinter · Pillow · stdlib
Срок
26 коммитов · ~месяц итераций
Итог
Бриф или ссылка → страница на хостинге
01 · Боль

Каждая новая страница — это копипаста предыдущей

Промо-страница под новый товар или новый язык делалась одинаково: продублировать папку предыдущей, пройтись руками по всей разметке и переписать то, что относится к старому проекту. Заголовок, meta description, весь блок og:*, фавикон, конфиг с названием магазина и ценами, вводные тексты, вопросы блока опроса, отзывы, блок предложения — и только потом изображения.

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

Финальный шаг тоже ручной: собрать папку в архив, зайти в панель хостинга, создать группу, загрузить zip, разобраться с конфликтом имён, если такая страница уже была. Один цикл — терпимо. Когда страниц несколько в неделю и на каждую есть варианты по языку и палитре — это уже не работа, а обслуживание собственной копипасты.

02 · Решение

Десктоп-приложение, которое читает источник и собирает страницу

Инструмент — обычное окно на tkinter с вкладками: проект, контент, палитра, изображения, опрос, отзывы, публикация, лог. Никакого сервера и сборки: python-файл запускается двойным кликом (обёртки под macOS и Windows лежат в репозитории). Логика одно-направленная — источник разбирается, шаблон заполняется, результат уезжает на хостинг.

01
Источник

Папка с материалами и брифом — либо ссылка: встроенный загрузчик снимает HTML и ассеты

02
Разбор

Заголовки, мета, названия, цены, тексты, вопросы, отзывы, палитра из CSS

03
Слоты

Авто-определение логотипа, фавикона, фона, товара по разметке; ручная правка в GUI

04
Сборка

Шаблон + контент → выходная папка, изображения пережимаются, разметка самопроверяется

05
Публикация

ZIP в память → Admin API хостинга: группа, имя по шаблону, конфликт → перезапись

Два семейства шаблонов — один интерфейс

В репозитории живут два шаблона: template_1 на Bootstrap и template_2 — статический экспорт Astro-сборки с атомарными утилитами в духе Tailwind. Разметка, имена файлов и набор слотов у них разные, поэтому при переключении пресета приложение перестраивает вкладки: свой набор слотов изображений, свой набор цветовых ключей, свои правила подстановки. Наружу — один и тот же сценарий работы.

Ссылка вместо папки: встроенный загрузчик

Источником контента может быть не только локальная папка, но и URL уже существующей страницы. Загрузчик написан на стандартной библиотеке и ведёт себя как аккуратный браузер:

  • учитывает <base href> при разрешении относительных путей
  • собирает ассеты из img/link/script/source/srcset и из url(...) в инлайн-стилях
  • рекурсивно обходит CSS: url() и @import — шрифты и фоны не теряются
  • качает только тот же origin — внешние CDN остаются ссылками, а не тянут за собой пол-интернета
  • перед разбором вырезает тела <script>, иначе строки внутри инлайн-JS ловятся как src=

Логотип, фавикон и фон определяются по разметке, а не по имени файла

Первая версия раскладывала картинки по слотам эвристиками «имя + размер файла» — и именно она подставляла в шапку случайный мусор. Сейчас приоритет отдан разметке: фавикон берётся из <link rel="icon">, логотип — из шапки и из alt/class со словами logo/brand, фон — из background-image: url(...) у body/html, картинки отзывов — из соответствующих блоков. Эвристики по имени и весу остались только запасным вариантом, а декоративные иконки, аватарки и бейджи вынесены в чёрный список — они больше никогда не претендуют на слот логотипа. Всё, что автомат не смог решить, остаётся видимым в интерфейсе и правится вручную.

Палитра снимается с источника, а не подбирается на глаз

Приложение читает HTML вместе с подключёнными CSS и достаёт цвета из переменных темы. Берётся последнее объявление — тема должна перекрывать базовые стили, а не наоборот. Токены нормализуются из hex, rgb() и oklch(), серое и почти белое отбрасывается как неинформативное, а оставшееся раскладывается по семантическим ключам: кнопка, текст кнопки, шапка, фон страницы, карточка, акцент. Дальше палитра доступна в интерфейсе как обычные поля — правится точечно.

Проект — это JSON, а не папка-снежок

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

Изображения пережимаются на выходе

Каждый файл, попадающий в слот, проходит через Pillow: ресайз по большей стороне, сохранение прозрачности для PNG/WebP, разумное качество на выходе. Если Pillow недоступен — тихий откат на обычное копирование, генерация не падает. Это одна из тех мелочей, которые невозможно дисциплинированно делать руками на каждой странице.

Самопроверка разметки перед публикацией

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

Публикация — одно действие, включая конфликт имени

Готовая папка пакуется в ZIP прямо в памяти (системный мусор вроде .DS_Store отсеивается), кодируется в base64 и уходит в Admin API хостинга. Список существующих групп подтягивается в выпадающий список, имя страницы собирается по шаблону вида {store} - {product} - {template} - {lang}. Ответ 422 «имя занято» не роняет процесс: приложение находит существующую запись и спрашивает, перезаписать ли её — при согласии уходит PUT по найденному id. Ключ доступа хранится в локальном конфиге с правами 600 вне репозитория.

Дизайн-система рядом с кодом

В репозитории лежат три брифа: общие основания (типографика, сетка, состояния, мобильная адаптация, бюджет производительности) и по одному документу на каждое семейство шаблонов — с разбором селектор за селектором, списками того, что удалить и что добавить, и чек-листом проверки. Смысл в том, что генератор гарантирует структуру, а брифы — визуальный уровень: иначе автоматизация просто быстрее тиражирует посредственную вёрстку.

03 · Стек

Десктоп без сборки — запускается двойным кликом

Python 3 + tkinter

Локальное приложение без сервера и без шага сборки

html.parser + regex

Разбор исходной страницы: мета, тексты, цены, вопросы, отзывы

urllib (stdlib)

Загрузчик страницы по URL и клиент Admin API хостинга

Pillow

Превью в интерфейсе и пережатие изображений на выходе

zipfile + base64

Упаковка результата в памяти и выгрузка архива по API

JSON-проекты

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

template_1 · Bootstrap

Первое семейство шаблонов со своим набором слотов

template_2 · Astro-экспорт

Второе семейство: статическая сборка на атомарных утилитах

design-briefs/

Дизайн-система: общие основания + бриф на каждое семейство

.app / run.sh / run.bat

Обёртки запуска — конечному пользователю Python не нужен

PythontkinterPillowhtml.parserurllibzipfileAdmin APIBootstrapAstroTailwind
04 · Результат

Страница собирается из проекта, а не из памяти

Сборка страницы
10 шагов 1 прогон

ручная правка разметки заменена одним запуском генерации

Публикация
1 действие

архив, группа, имя и конфликт имени — внутри инструмента

Эволюция инструмента
26

коммитов — каждая правка пришла из реальной сборки, а не из плана

Ушёл целый класс ошибок, а не просто время

Забытый og:image, фавикон от прошлого проекта, логотип не в том слоте, картинка на несколько мегабайт, потерянный при переносе фон — всё это раньше ловилось глазами уже на живой странице. Теперь эти вещи либо выведены в интерфейс как явные поля, либо проверяются автоматически перед выгрузкой.

История коммитов — это список пойманных граблей

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

Честные границы

Это внутренний инструмент, а не продукт: интерфейс не покрыт UI-тестами, а качество разбора напрямую зависит от аккуратности разметки источника. На нестандартном источнике автомат заполняет часть полей мимо — поэтому всё, что он определил, остаётся редактируемым, а лог генерации показывает каждый шаг. Такой контур честнее «магии»: пользователь всегда видит, что именно подставлено и откуда.

05 · Применимость

Где ещё ложится та же методология

Это кейс не про лендинги. Это типовая задача «шаблон + структурированный контент → готовый статический артефакт → автопубликация». Контур переносится почти без изменений везде, где однотипные страницы делаются руками:

  • Мультиязычные и мультибрендовые страницы — одна сборка, разные проекты: язык, палитра, логотип, тексты
  • Посадочные под каждый SKU из товарного фида — генерация партиями вместо ручной вёрстки карточек
  • Тиражирование сайтов сети или франшизы — общий шаблон, свои контакты, цвета и логотип на каждую точку
  • Перенос существующей страницы на новый шаблон — загрузчик снимает исходник, парсер вытаскивает контент и палитру
  • Пакетная подготовка вариантов для сравнительных тестов — те же тексты, другая палитра и слоты, публикация одной очередью
  • Тот же код без интерфейса — как CLI-шаг в CI: проект в JSON на входе, опубликованная страница на выходе
Что переиспользуется на следующих проектах
  • Загрузчик статической страницы: рекурсивный обход CSS, ограничение по origin, сохранение структуры путей
  • Слой определения ролей изображений: сначала разметка, потом эвристика, всегда — ручная правка в интерфейсе
  • Снятие палитры из CSS-переменных: hex / rgb / oklch, приоритет темы над базой, отсев серого
  • Формат проекта в JSON — сборка воспроизводима, версионируется и переиспользуется как основа варианта
  • Самопроверка обязательных маркеров разметки перед публикацией — с восстановлением и предупреждением в логе
  • Публикация в хостинг/CMS по API: ZIP в памяти, base64, обработка конфликта имени через перезапись
Похожая задача?

Если страницы делаются копипастой предыдущей — это превращается в генератор

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

Готовы начать?

Аудит за 5 000 ₽ — с конкретным отчётом и сметой

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

Или просто напишите свой вопрос — отвечу в течение 2 часов