Desktop promo-landing generator: brief or URL → finished page
My own tool, written for my own workflow. Promo pages used to be copy-pasted from the previous one — with manual edits to meta tags, copy, image slots and palette. I built a desktop app that assembles the page from a template and a brief (or lifts the content off an existing page by URL) and ships the finished archive to the host over its API.
Every new page was a copy-paste of the last one
A page for a new product or a new language was always built the same way: duplicate the previous
folder, walk the markup by hand and rewrite everything that belonged to the old project. Title,
meta description,
the whole og:*
block, favicon, the config holding store name and prices, intro copy, survey questions, reviews,
the offer block — and only then the images.
Images were their own failure mode. The template has fixed slots: intro logo, header logo, favicon, page background, product shot, social image. Files from the source arrive mixed in with decorative icons, avatars and badges — and the logo slot kept getting the wrong file. Nobody watches file weight either: a multi-megabyte original ships to production untouched.
The last step was manual too: zip the folder, open the host panel, create a group, upload the archive, then deal with the name clash if that page already existed. One cycle is tolerable. Several pages a week, each with language and palette variants, and it stops being work and becomes maintenance of your own copy-paste.
A desktop app that reads the source and assembles the page
The tool is a plain tkinter window with tabs: project, content, palette, images, survey, reviews, publishing, log. No server, no build step — the Python file launches on a double click, with macOS and Windows wrappers in the repo. The flow is one-directional: parse the source, fill the template, push the result to the host.
A folder with assets and a brief — or a URL: the built-in fetcher pulls HTML and assets
Titles, meta, names, prices, copy, questions, reviews, palette from CSS
Logo, favicon, background and product auto-detected from markup; editable in the GUI
Template + content → output folder, images compressed, markup self-checked
In-memory ZIP → host Admin API: group, templated name, clash → overwrite
Two template families, one interface
The repo carries two templates:
template_1
on Bootstrap, and
template_2
— a static export of an Astro build styled with Tailwind-style atomic utilities. Their markup, file
names and slot sets differ, so switching the preset rebuilds the tabs: its own image slots, its own
colour keys, its own substitution rules. From the outside it stays one workflow.
A URL instead of a folder: the built-in fetcher
The content source can be an existing page's URL rather than a local folder. The fetcher is written on the standard library and behaves like a well-mannered browser:
- honours
<base href>when resolving relative paths - collects assets from
img/link/script/source/srcsetand fromurl(...)in inline styles - walks CSS recursively —
url()and@import, so fonts and backgrounds aren't lost - stays on the same origin — external CDNs remain links instead of dragging half the internet along
- strips
<script>bodies before parsing, otherwise strings inside inline JS get matched assrc=
Logo, favicon and background come from the markup, not the filename
The first version sorted images into slots with "name + file size" heuristics — and that is exactly
what dropped random junk into the header. Markup now takes priority: the favicon comes from
<link rel="icon">,
the logo from the header and from alt/class
values containing logo/brand, the background from
background-image: url(...)
on body/html,
review images from their own blocks. Name and weight heuristics survive only as a fallback, and
decorative icons, avatars and badges sit on a deny-list so they can never claim the logo slot again.
Anything the tool can't decide stays visible in the interface and gets fixed by hand.
The palette is lifted from the source, not eyeballed
The app reads the HTML together with its linked CSS and pulls colours out of theme variables. The
last declaration wins — a theme is supposed to override base styles, not the other way round. Tokens
are normalised from hex,
rgb()
and oklch(),
greys and near-whites are dropped as uninformative, and what remains is mapped onto semantic keys:
button, button text, header, page background, card, accent. From there the palette is just editable
fields in the UI.
A project is a JSON file, not a snowballing folder
Build state is saved to a project file: chosen template, paths, all content, palette, slot mapping, questions, reviews. A repeat or a variant means opening the project and editing two fields instead of cloning a directory. Side effect: builds are reproducible and can be versioned alongside the code.
Images get compressed on the way out
Every file landing in a slot goes through Pillow: resize on the longer edge, transparency preserved for PNG/WebP, sane output quality. If Pillow isn't available it falls back quietly to a plain copy — generation never fails on this. It is exactly the kind of small discipline nobody keeps by hand on every page.
The build self-checks its markup before publishing
A finished page has mandatory pieces that are easy to lose while editing a template: hidden placeholders the host substitutes at serve time, and a shared event identifier for the browser-side and server-side analytics paths. The generator verifies they exist in the output, re-injects whatever is missing and writes a warning into the log. The reasoning is simple: a template edit must not silently break event tracking — silent zeros surface far too late.
Publishing is one action, name clash included
The finished folder is zipped in memory (system junk such as
.DS_Store
is filtered out), base64-encoded and pushed to the host Admin API. Existing groups are pulled into a
dropdown; the page name is composed from a pattern like
{store} - {product} - {template} - {lang}.
A 422
"name already used" response doesn't kill the run: the app locates the existing record and asks
whether to overwrite it, then sends a
PUT
to that id. The API key lives in a local config with
600
permissions, outside the repository.
A design system that lives next to the code
The repo also holds three briefs: shared foundations (typography, grid, states, mobile adaptation, performance budget) plus one document per template family — a selector-by-selector breakdown, a delete list, an add list and a verification checklist. The point is division of labour: the generator guarantees structure, the briefs guarantee the visual bar. Otherwise automation just ships mediocre layout faster.
Desktop, no build step — it opens on a double click
Local app with no server and no build pipeline
Source parsing: meta, copy, prices, questions, reviews
Page fetcher by URL and client for the host Admin API
In-app previews and image compression on output
In-memory packaging and archive upload over the API
Full build state in a file — reproducibility and variants
First template family with its own slot set
Second family: static build on atomic utilities
Design system: shared foundations + a brief per family
Launch wrappers — the end user never touches Python
Pages get built from a project, not from memory
manual markup editing replaced by a single generation run
archive, group, name and name clash all handled inside the tool
commits — each fix came out of a real build, not a roadmap
A whole class of mistakes disappeared, not just the hours
A forgotten og:image,
last project's favicon, a logo in the wrong slot, a multi-megabyte photo, a background lost in the
move — all of it used to be caught by eye on a live page. Those things are now either surfaced as
explicit fields in the interface or verified automatically before upload.
The commit history is a list of rakes stepped on
The tool was hardened on real builds, and nearly every commit is a specific defect: an intro logo growing sub-pixel by sub-pixel because of an animation; relative paths breaking on transfer; a name clash on re-upload that used to abort the run outright; small icons the heuristic mistook for a logo. You don't design these in advance — you find them in production use, which is precisely why a tool for your own process is worth writing yourself.
Honest limits
This is internal tooling, not a product: the interface has no UI test coverage, and parsing quality depends directly on how disciplined the source markup is. On an unusual source the automation fills some fields wrong — which is why everything it detects stays editable and the generation log shows every step. That contract is more honest than magic: you always see what was substituted and where it came from.
Where the same methodology applies
This case isn't about landing pages. It's the standard problem of "template + structured content → finished static artefact → automatic publishing". The pattern transfers almost unchanged anywhere repetitive pages are built by hand:
- → Multilingual and multi-brand pages — one build, different projects: language, palette, logo, copy
- → A landing page per SKU from a product feed — batch generation instead of hand-built cards
- → Rolling out chain or franchise sites — shared template, per-location contacts, colours and logo
- → Migrating an existing page to a new template — the fetcher lifts the source, the parser extracts content and palette
- → Batch-prepared variants for comparison tests — same copy, different palette and slots, published in one queue
- → The same code without a GUI — as a CLI step in CI: a JSON project in, a published page out
- A static-page fetcher: recursive CSS walk, same-origin limit, preserved path structure
- An image-role detection layer: markup first, heuristics second, manual override always available
- Palette extraction from CSS variables: hex / rgb / oklch, theme over base, greys filtered out
- A JSON project format — builds are reproducible, versionable and reusable as the basis for a variant
- A pre-publish self-check of mandatory markup markers — re-injection plus a warning in the log
- Publishing to a host/CMS over its API: in-memory ZIP, base64, name-clash handled as an overwrite
If your pages are copy-pasted from the last one — that converts into a generator
Tooling built around your own process pays off wherever a repetitive page is produced weekly rather than quarterly. Start with one template, one source format and one publishing target — the rest grows out of real builds.
The 5,000 ₽ audit — with a concrete report and quote
I'll tell you what to deploy in your business first, what the payback looks like, and whether you need AI for the task at all (sometimes you don't).
Or just send your question — I reply within 2 hours