Client project
DanceHub
A dance studio site and an enquiry bot: booking in one tap straight from the timetable, with no form filling and no back-and-forth.
- lines — the whole site
- 5,613
- lines — the whole bot
- 226
- npm packages
- 0

A dance studio near Minsk: four disciplines, a weekly timetable, and booking a class. The site is a single self-contained file; enquiries are handled by a Telegram bot of just over two hundred lines.
The whole site is one file
Markup, styles, scripts and all the artwork live in a single index.html of 5,613 lines. No
build step, no npm, not one package. Exactly two external requests: the fonts and the map widget
in the contacts section.
This is not an exercise in minimalism. The studio owner has no developer on retainer, and the site has to still open three years from now — and three years from now there is nothing to rebuild and nothing to go stale.
The owner edits the site without opening code
At the end of the file there is a single settings block: phone number, messenger handles, address and landmark, opening hours, message templates, section switches. The values distribute themselves across the page — the markup is tagged with attributes and the script fills them in.
An empty field does not leave a dead link, it removes the element entirely, along with its row in the contacts list. The call, WhatsApp and Viber buttons disappear when there is no number.
Booking in one tap from the timetable
The whole point of the project. Tapping a timetable row fills the form with the discipline, the day and the time — nobody re-picks them from dropdowns.
There is a detail here that only shows up in a screen-reader pass: the button’s accessible name is built from the row’s own data. “19:00 BODY FLEX” occurs twice in the week, and without that a blind visitor would silently book the wrong day.
The timetable lives in the markup, not in a script
Classes are plain markup, not an array in JavaScript. The filter across four disciplines recounts the result, today’s column is highlighted, and when there is nothing on today, the line about the next class day is derived from the timetable itself rather than typed in by hand.
A side benefit: search engines read the timetable even when the script did not run.
Picking a discipline in three questions
Someone who has never danced does not know the difference between POLE EXOT and DANCE PLASTIC. Three questions, scored in the browser, and the result leads either to the form with the discipline already chosen or to the timetable filtered to match the answer.
The bot answers the site before Telegram answers the bot
An enquiry goes to POST /api/lead. The bot replies to the site immediately and hands delivery
to the operators off to a background thread: the visitor does not wait for Telegram.
If the server is down, the site does not show a “failed to send” error — it offers an “Open Telegram” button. Nobody should be lost because a process of ours fell over.
The form is shielded by a hidden trap field: if it is filled in, the response is a quiet success and nothing is sent anywhere.
Conversations through the bot, not in a shared chat
A client writes to the bot, the message reaches every operator tagged with who sent it, an operator answers with a plain Reply to the forwarded message, and the bot delivers that answer to that exact client.
Operators have no new interface to learn — they work in Telegram the way they already do. The message mapping lives in a state file and is trimmed to the last four thousand entries so the file cannot grow forever.
The bot is written on the bare Python standard library: three attempts per Telegram call,
retry_after handling on rate limits, atomic state writes, a polling offset that survives
restarts, and an exception in one message that does not take the loop down.
Search is built into the markup, not into a script
Two schema.org blocks: a linked graph of site → page → studio → instructor, and a separate questions-and-answers block. The studio card carries the address, postcode, phone, coordinates, map, service areas and descriptions of all four disciplines.
A script reconciles the phone and address with the settings if the owner changes them, but the source of truth is the markup itself: Yandex’s crawler reads the source and does not always run JavaScript. For the same reason the address and phone are written on the page as plain text rather than injected by script.
All twelve questions have their own anchors — a link like that is handy to paste straight into a conversation.
The artwork is drawn, not photographed
There is not one raster photograph on the site: the first screen, the discipline cards and the studio are vector artwork in the same file. That was deliberate — the studio is young, and stock photos of somebody else’s hall are recognised instantly and work against trust.
When real shots exist, no code has to change: files are picked up by fixed names, and if a file is missing the image is removed and the artwork stays.
A printed flyer
An A6 flyer to hand out: two artboards plus a frugal version for uncoated paper, 3 mm bleed, and no text closer than 4.9 mm to the trim line. The print shop gets ready PDFs and PNGs.
HTML · CSS · JavaScript · SVG · Python (standard library) · Telegram Bot API · nginx · systemd