A desktop app builds a promo page from a brief or a link
I built this tool for my own work. Every promo page used to be a copy of the previous one, with meta tags, copy, images and colors edited by hand. Now the app builds the page from a template and a brief, or takes the content of an existing page by its URL, and uploads the finished archive to the host by itself.
Every new page was a copy of the last one
A promo page for a new product or a new language was always made the same way: copy the previous
page's folder, go through all the markup by hand and rewrite everything that belonged to the old
project. The title, the
meta description,
the whole og:*
block (it controls how a link looks on social media), the favicon, the config file with the store
name and prices, intro copy, survey questions, reviews, the offer block, and only then the images.
Images caused the most mistakes. The template has fixed places for them: the logo on the first screen, the header logo, the favicon, the page background, the product photo and the social media image. Files from the source arrive mixed with decorative icons, avatars and badges, and the logo slot kept getting the wrong file. Nobody watched file sizes either: a source image of several megabytes went to the live site as it was.
The last step was manual too: zip the folder, open the hosting panel, create a group, upload the archive, and sort out the name clash if the page already existed. Once, that's bearable. With several pages a week, each with language and color variants, the whole job turns into keeping track of copies of copies.
A desktop app that reads the source and builds the page
The app is a plain window built with tkinter, Python's standard toolkit for windows. It has tabs: project, content, colors, images, survey, reviews, publishing, log. There is no server and no build step: the app opens with a double click, and launch files for macOS and Windows are in the repository. Work flows one way: the source is parsed, the template is filled in, and the result goes to the host.
A folder with files and a brief, or a URL: the built-in downloader fetches the HTML and all the page's files
Titles, meta tags, names, prices, copy, questions, reviews, colors from CSS
The app finds the logo, favicon, background and product photo in the markup; you can correct them in its window
Template and content go into an output folder, images are compressed, the markup is checked automatically
The archive is packed in memory and sent to the host through its API: group, name from a pattern, overwrite on a name clash
Two template families, one interface
The repository has two templates:
template_1
on Bootstrap, and
template_2,
a static export of an Astro build with Tailwind-style utility classes. Their markup, file names and
sets of slots differ, so when you switch templates the app rebuilds its tabs: each template has its
own image slots, its own set of colors and its own substitution rules. For the user, the workflow
stays the same.
A page from a link: the built-in downloader
The source can be a folder on your computer or the address of an existing page. The downloader uses only the Python standard library and behaves like a careful browser:
- honors
<base href>when resolving relative paths - collects files from
img/link/script/source/srcsetand fromurl(...)in inline styles - follows CSS all the way down, including
url()and@import, so fonts and backgrounds aren't lost - downloads files only from the same site; files on external CDNs stay as links, so the download doesn't pull in half the internet
- strips the contents of
<script>before parsing; otherwise strings inside inline JS get mistaken forsrc=
The logo, favicon and background come from the markup
The first version sorted images into slots by guessing from file name and size, and that is how
random images ended up in the header. Now the markup comes first. 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,
and review images from their own blocks. Guessing by name and size is left only as a fallback.
An exclusion list keeps decorative icons, avatars and badges out of the logo slot. Anything the app can't work out stays visible in the interface and is fixed by hand.
Colors are taken from the source page
The app reads the HTML together with its linked CSS files and pulls colors out of the theme
variables. If a color is set several times, the last value wins, because the theme should override
the base styles. Colors in hex,
rgb()
and oklch()
are brought to one format. Greys and near-whites are dropped, and the rest are assigned to roles:
button, button text, header, page background, card, accent. After that, the colors appear in the
interface as ordinary fields you can edit.
A project is a single JSON file
Everything about a build is saved in a project file: the chosen template, paths, all the content, colors, which image goes in which slot, questions, reviews. To repeat a page or make a variant, you open the project and change a couple of fields. As a bonus, any build can be repeated exactly, and its versions can be kept alongside the code.
Images are compressed before publishing
Every file that goes into a slot passes through Pillow, a Python library for working with images. It shrinks the image along its longer side, keeps transparency for PNG and WebP and picks a sensible quality. If Pillow isn't available, the files are simply copied and the build carries on. This is the kind of small chore nobody does reliably by hand on every page.
Before publishing, the app checks the markup
A finished page has required elements that are easy to lose while editing a template: hidden placeholders that the host fills in when it serves the page, and a shared event ID for analytics in the browser and on the server. The app checks for them before upload. If something is missing, it puts it back and writes a warning to the log. A template edit must not quietly break event tracking: when statistics silently show zeros, people notice far too late.
Publishing takes one action, even when the name is taken
The finished folder is zipped in memory (system files such as
.DS_Store
are filtered out), encoded in base64 and sent to the host's Admin API. Existing groups load into a
dropdown, and the page name is built from a pattern like
{store} - {product} - {template} - {lang}.
If the host replies with a 422
"name already used" error, the run doesn't stop: the app finds the existing record and asks whether
to overwrite it. If you agree, it sends a
PUT
request to that record's id. The API key is kept outside the repository, in a local config file
that only the owner can read (permissions
600).
The design rules live next to the code
The repository also holds three briefs. The first covers the shared basics: fonts, grid, element states, the mobile version and the page weight budget. The other two are written one per template family: they go through every CSS selector, list what to remove and what to add, and end with a checklist. The app takes care of structure, and the briefs take care of the look. Without the briefs, the app would simply turn out weak layouts faster.
A desktop app with no build step: it opens with a double click
A local app with no server and no separate build step
Parses the source page: meta tags, copy, prices, questions, reviews
Downloads pages by URL and talks to the host's Admin API
Previews in the interface and image compression before publishing
Packs the result in memory and uploads the archive through the API
All build settings live in one file, so a page can be repeated or turned into a variant
The first template family, with its own set of slots
The second family: a static Astro build with Tailwind-style classes
Design rules: shared basics and a brief for each family
Launch files for macOS and Windows: the app opens with a double click
Pages are built from a project file
one run of the app replaced manual markup editing
the app handles the archive, group, name and name clash
commits, and each fix came out of a real build
A whole class of mistakes is gone
A forgotten og:image,
the favicon from the last project, a logo in the wrong slot, a photo of several megabytes, a
background lost in the move: all of this used to be spotted by eye on a live page. Now these things
either appear as separate fields in the interface or are checked automatically before upload.
The commit history is a list of real bugs
The tool was polished on real builds, and nearly every commit fixes a specific bug. An animation made the intro logo grow by fractions of a pixel. Relative paths broke when files were moved. A name clash on re-upload used to stop the run outright. The app mistook small icons for a logo. You can't foresee things like these; they turn up in real use. That is why a tool for your own process is worth refining on live tasks.
Limits
This is an internal tool. The interface has no automated tests, and parsing quality depends directly on how neatly the source page is built. On an unusual page the app fills some fields wrongly. That is why everything it detects can be edited, and the log shows every step. You always see what was filled in and where it came from.
Where else this approach works
Behind this case is a common task: take a template and content, build a finished page and publish it with no manual steps. The same pattern works almost unchanged wherever similar pages are built by hand:
- → Pages in several languages and for several brands: one app, with a separate project for each language, color scheme, logo and copy
- → A landing page for each product: pages built in batches straight from the product feed
- → Sites for a chain or franchise: a shared template with each location's own contacts, colors and logo
- → Moving an existing page to a new template: the downloader fetches the source, and the parser pulls out the content and colors
- → Variants for comparison tests: the same copy with different colors and images, published in one queue
- → The same code without a window: as a step in automated builds (CI), with a JSON project in and a published page out
- A page downloader that follows CSS all the way down, stays on the same site and keeps the original folder structure
- Sorting images into slots: markup first, then name and size, with manual correction always available
- Colors from CSS variables: hex, rgb and oklch, the theme overrides base styles, greys are filtered out
- A JSON project format: builds can be repeated, versioned and used as the basis for a variant
- A pre-publish check of required markers in the markup: missing ones are restored and a warning goes to the log
- Publishing to a host or CMS through its API: archive in memory, base64, overwrite on a name clash
If every page is a copy of the last one, a program can build them for you
A tool built around your own process pays off where similar pages go out every week. Start with one template, one source format and one place to publish. The rest gets added as real work comes in.
Related cases and ready-made solutions
An app for an online school that reminds students about lessons and helps them practise pronunciation
A SwiftUI app for iOS 17 and later reminds students about each class a day, an hour and 10 minutes ahead…
A fitness studio app that installs from the browser and updates without the App Store
A progressive web app (PWA) installs on the iPhone home screen straight from Safari, works offline and sends…
An Android app: embedded browser, APK analysis and release preparation
Native Kotlin screens, a WebView with files and permissions, and state restoration. Partial APK analysis…