Skip to content

Allow this site to count your visits with Yandex Metrica? Metrica will store cookies in your browser to do this. How the site uses cookies

VC
Case 25 of 25 · Web · Internal tools

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.

Project type
My own tool · internal build
Technology
Python · tkinter · Pillow
Timeline
About a month of work, 26 commits
Outcome
Brief or URL → page live on the host
01 · Problem

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.

02 · Solution

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.

01
Source

A folder with files and a brief, or a URL: the built-in downloader fetches the HTML and all the page's files

02
Parsing

Titles, meta tags, names, prices, copy, questions, reviews, colors from CSS

03
Slots

The app finds the logo, favicon, background and product photo in the markup; you can correct them in its window

04
Build

Template and content go into an output folder, images are compressed, the markup is checked automatically

05
Publish

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/srcset and from url(...) 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 for src=

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.

03 · Technology

A desktop app with no build step: it opens with a double click

Python 3 + tkinter

A local app with no server and no separate build step

html.parser + regex

Parses the source page: meta tags, copy, prices, questions, reviews

urllib (stdlib)

Downloads pages by URL and talks to the host's Admin API

Pillow

Previews in the interface and image compression before publishing

zipfile + base64

Packs the result in memory and uploads the archive through the API

JSON projects

All build settings live in one file, so a page can be repeated or turned into a variant

template_1 · Bootstrap

The first template family, with its own set of slots

template_2 · Astro export

The second family: a static Astro build with Tailwind-style classes

design-briefs/

Design rules: shared basics and a brief for each family

.app / run.sh / run.bat

Launch files for macOS and Windows: the app opens with a double click

PythontkinterPillowhtml.parserurllibzipfileAdmin APIBootstrapAstroTailwind
04 · Results

Pages are built from a project file

Building a page
10 steps 1 run

one run of the app replaced manual markup editing

Publishing
1 action

the app handles the archive, group, name and name clash

How it evolved
26

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.

05 · Where it fits

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
What I reuse on later projects
  • 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
Have a similar task?

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.