BoudaKaci
Rizikové kácení, prořezy stromů a práce v lese v Plzeňském kraji — web Tomáše Boudy.
Bouda kácí
Website of Tomáš Bouda's tree-felling and tree-care business — https://boudakaci.cz.
The pages are hand-written HTML, CSS and JavaScript with no framework and no build step. A small ASP.NET Core application (.NET 10) serves them and adds what a static host could not:
POST /api/poptavka— the contact form, e-mailed through Resend, with a confirmation to a customer who left an e-mail, or a text to one who left a mobile number (through SMSbrána.cz, SMS Connect API,Site:Sms:Login/Password)POST /api/resend/inbound— Resend's signed webhook (Svix) for mail received at any address of boudakaci.cz (the domain's MX points to Resend); the e-mail with its attachments is fetched from Resend's receiving API and forwarded toSite:Resend:Towith the original sender as reply-to. NeedsSite:Resend:WebhookSecretand a full-access API key; once set, customers reply to the confirmation atpoptavka@boudakaci.cz- When to fell
kdy-kacet.html— the months for felling and a seasonal scene (kdy-kacet.js: sawing in the dormant season, checking the nest with binoculars while birds nest, measuring the trunk for the permit in autumn, a Christmas tree in December;?mesic=1–12opens another month) - Advice
/radyand/rady/{slug}— articles written as Markdown inContent/rady/*.md(front mattertitle,description,slug; a## Časté otázkysection with### questionitems becomes the FAQ, anything after it closes the article). Rendered with Markdig intoTemplates/rada.htmland listed in the sitemap; a new file is picked up without a restart - Structured data — the home page and the articles carry an
FAQPagebuilt from their own FAQ markup, and the LocalBusinessareaServedfollows the districts served set in the admin (Site/StructuredData.cs) - Contacts kept from harvesters — no page carries the phone or the e-mail (not even the structured data): the links
lead to
/#kontaktand show a mask („605 870 •••“). After the first genuine interaction of a person (mouse movement, touch, key, wheel;isTrusted)script.jsasks/api/kontakt(fromSite:Business) and fills in thetel:, WhatsApp andmailto:links and the shown numbers; a tap before that waits for them and then dials. The same idea as in the arborist catalogue (Arboriste,ContactGuard) - Price page
cenik.htmland permit guidepovoleni-ke-kaceni.html; prices come from the admin - Municipality pages
/kaceni-stromu/{obec}built from the published jobs (templateTemplates/obec.html), and a generated/sitemap.xml - Map page
mapa.html— the districts of Plzeňský kraj the business serves and the municipalities where work is done, on Mapy.com tiles - Admin
/admin— one password: jobs from the inquiry to the finished work, with their photos (map + gallery), felling permit applications (PDF for the municipal office), the price list and the served districts GET /health— status and version for the deploy and the VPS dashboard- a log (daily files + the admin Logs page)
It runs as one Docker container on the VPS (37.205.12.163) behind the shared nginx.
Repository
| Path | What |
|---|---|
src/BoudaKaci.Web/wwwroot/ |
the site: index.html, galerie.html, mapa.html, 404.html, styles.css, script.js, mapa.js, assets/ |
src/BoudaKaci.Web/ |
the application: page serving, contact form, jobs (SQLite), Mapy.com proxy, admin (Razor Pages) |
tests/BoudaKaci.Web.Tests/ |
xUnit tests of the whole site in memory |
deploy/ |
Dockerfile, deploy-vps.sh (runs on the VPS), nginx template |
originals/ |
full-size source photos and the icon master; not deployed |
tools/ |
placeholder check (pipeline) and the font downloader |
The pages carry two placeholders the application fills in when serving them: __GA_MEASUREMENT_ID__
(GA4 ID from configuration; anything else leaves analytics off) and __APP_VERSION__ (from the build).
Running locally
dotnet run --project src/BoudaKaci.Web --launch-profile boudakaci
The site answers on http://localhost:5270. Settings come from appsettings.json, overridden by
src/BoudaKaci.Web/appsettings.Local.json (git-ignored) or environment variables:
| Setting | Environment variable | What |
|---|---|---|
Site:AdminPassword |
Site__AdminPassword |
password of /admin; empty keeps the admin locked |
Site:MapyApiKey |
Site__MapyApiKey |
Mapy.com REST API key; without it development falls back to OpenStreetMap tiles and the municipality search is off |
Site:TreeMap:BaseUrl |
Site__TreeMap__BaseUrl |
origin of TreeMap (default https://treemap.cz); the deep link, the stored share links and the share API point there. Links on the old treemap.tomlabs.xyz (and www.treemap.cz) are accepted and rewritten to it (Permits/TreeMapLinks.cs; stored ones moved by schema 13) |
Site:TreeMap:JobsProject |
Site__TreeMap__JobsProject |
the top-level TreeMap project (UUID or exact name, default Zakázky) under which a job's tree project is created |
Site:Business:BaseAddress |
Site__Business__BaseAddress |
where trips start, for a visit's driving distance and the bias of the municipality search; empty = Site:Business:Address |
Site:Resend:ApiKey |
Site__Resend__ApiKey |
Resend key for the contact form |
Site:GaMeasurementId |
Site__GaMeasurementId |
GA4 ID (G-…) for tomlabs-consent.js |
Site:DataDirectory |
Site__DataDirectory |
jobs database boudakaci.db and logs/ (default data/, git-ignored) |
The API keys never reach the browser: tiles (/api/tiles/{z}/{x}/{y}) and the municipality search
(/admin/api/obce) go through the application.
Tests
dotnet test BoudaKaci.slnx
They cover the contact form (validation, honeypot, photo limits, HTML escaping, rate limit, Resend failures), page serving with the placeholders, security and cache headers, health, the tile proxy, sign-in and the job admin down to what the public map shows. The pipeline runs them before every deploy.
Site visits and the price calculator
A job has a site visit („obhlídka“, Visits/, tables site_visits and visit_trees, schema 10) at
/admin/zakazka/{job}/obhlidka: the facts Tomáš collects at the tree — date, address, one-way distance, access
(trailer to the tree or carrying so many metres), wires nearby (power lines show a red warning: he does not work
near them), and one card per tree: species (picker from Permits/TreeNames.cs), height, circumference at 130 cm
(the diameter is derived), the fruit-tree exemption, the kind of work (FellGround, FellSectional, Prune,
DeadBranches, Reduction), what is near the tree, the state of the wood, what happens to the wood and brush,
the stump height, the hours on the tree and of cleanup (prefilled from the settings by kind and height band, in
admin.js and again on the server when posted empty) and whether a helper is needed. VisitTree.NeedsPermit is
derived: circumference over 80 cm, not an exempt fruit tree, and the tree is to be felled. The form is built for a
phone (one column under 700 px, plain POST, no script needed).
Visits/VisitCalculator.cs is pure and turns a visit, the calculator settings, the felling ladder and the price
cards into proposed quote lines, each with an explanation: felling from the ground is the ladder row for the
diameter (the label is parsed: „do 30 cm“, „31–50 cm“, „nad 70 cm“); sectional felling, or any felling near a
target, is charged as hours of Tomáš and the helper with the risky row as the floor plus a risk surcharge in percent
of that row; pruning is the „Prořez“ card per tree, or the same hourly scheme when climbing (over 6 m or near a
target); cleanup is the sum of processing hours at the hourly rate; carrying, hauling („Odvoz“ card per trailer
load) and travel beyond the included kilometres follow. Bad wood adds its percentage; estimated work lines are
rounded to the settings' step; travel under 100 CZK is dropped with a note. The page shows the total and the margin
check (price per hour on site against the hourly rate). Settings live in Ceny → „Kalkulačka obhlídky“ (table
calculator_settings, one JSON document, Visits/CalculatorSettings.cs holds the defaults from the October 2026
price sheet). tests/BoudaKaci.Web.Tests/VisitCalculatorTests.cs pins the worked examples A–E of that sheet.
„Vytvořit nabídku z obhlídky“ saves the form and opens a new quote with the proposed lines (VAT handling as any
quote); „Založit žádost o povolení“ starts a permit application with the trees that need one, the job's customer as
owner and the visit's TreeMap link. When the visit has a TreeMap share link, the page re-reads the share on every
opening (and on „Načíst stromy z TreeMapu“): one tree per shared tree keyed by TreeMap's tree id — new ones are
added with species, circumference, height, the note „TreeMap strom N, štítek …“ and the kind of work mapped from the
proposed measure (S-KV/S-KSP → from the ground, S-KPV/S-KPP → in parts, S-RB → dead branches, reductions
→ reduction, other S-R*/O-* → pruning; measures already done or deleted are ignored); existing ones get their
species, circumference and height refreshed, their pricing facts are never overwritten, nothing is deleted. A
share that does not answer leaves the stored trees and shows a quiet note.
The round trip with TreeMap (0.19+) needs no copying of links. While the visit has no share link, „Otevřít v
TreeMapu“ is the deep link {Site:TreeMap:BaseUrl}/#new?name=<job title>&parent=<Site:TreeMap:JobsProject>&return=<absolute URL of the visit page> (Permits/TreeMapLinks.cs; the return URL is built from the request, so it works on localhost
too). TreeMap creates — or reuses by exact name under that parent — the job's project, makes its share token and, on
„Zpět do administrace“, navigates back to the visit page with ?share=<token>. The page validates the token (24
characters of [A-Za-z0-9_-]), stores {BaseUrl}/#share=<token> as the visit's TreeMap link (creating the visit row
when none exists yet), runs the usual sync and redirects to the clean URL with the note „Projekt z TreeMapu připojen,
stromy načteny“; an invalid token is ignored with a quiet note. Once a share link is stored, the plain „Otevřít v
TreeMapu ↗“ opens it and the deep link stays available as „Založit nový projekt v TreeMapu“. TreeMap accepts only
boudakaci.cz and localhost as return hosts (RETURN_HOSTS in its app/src/config.js).
Customer source, inquiry details and statistics
A job carries Source (Jobs/JobSource.cs: google, seznam, bazos, web-direct, referral, municipality, facebook,
partner, repeat, other; schema 12) and a free-text SourceNote, edited on the job form and shown as a chip in the
job list, which /admin?zdroj=<source> filters (- = jobs without a source).
An inquiry from the contact form is attributed automatically. script.js remembers, for the browser session only
(sessionStorage), the first page of the visit with its query string, document.referrer and utm_source /
utm_medium / utm_campaign, and posts them with the inquiry (landingPage, referrer, utm* — hidden fields,
nothing the visitor sees changes). JobSource.FromAttribution maps them: utm_source wins when present, otherwise
the referrer's host (google.* → Google, seznam.cz / firmy.cz / mapy.cz → Seznam, bazos.cz → Bazoš, facebook.* /
instagram.* → Facebook, no referrer or boudakaci.cz → web přímo, anything else → jiné).
Alongside, Inquiries/InquiryMeta.cs stores non-identifying details of the visit in inquiry_meta (one row per
inquiry job): the device class (phone / tablet / desktop), the browser family with its major version, the OS family,
the primary language tag of Accept-Language, the landing page, the referrer, the UTM values and the timestamp. The
IP address and the raw user-agent string are not stored (the address only passes through the in-memory rate
limiter as before). The details are shown only on the job detail in the admin, collapsed as „Technické údaje
poptávky“; they are not in the public API (/api/galerie), the quote or the log, and they go with the job when it is
deleted.
/admin/statistiky (Jobs/JobStatistics.cs, pure) shows per month for the last 12 months (with a CSS bar each)
and per year: inquiries (jobs by their month), accepted, done and rejected, revenue (the latest quote of each done
job; 0 without a quote), hours on site (the visit's estimated hours) and realised CZK/h over the done jobs that have
both; the sources with count, done, conversion and revenue; the devices of inquiries from the form. „Stáhnout CSV“
(?handler=Csv) exports the jobs with source, quote total, hours and device as a semicolon-separated UTF-8 file.
Places, distances and PDF text
- Municipality search (
/admin/api/obce,Maps/MapyClient.cs): Mapy.com suggest withtype=regional.municipalityandtype=regional.municipality_part,limit=15,locality=czandpreferNear= the geocoded base address (Plzeň when it cannot be geocoded); answers in Plzeňský kraj are listed first, a part shows the municipality it belongs to („Horušany — část obce Nýřany, okres Plzeň-sever, Plzeňský kraj“), and the main part of a municipality is not listed twice. - Visit address and distance: a new visit's address starts as the job's municipality (a job keeps no street; the
street goes into the job notes and is added on the visit). On save, while the distance is empty or the address
changed without a new distance typed, the one-way driving distance is computed through Mapy.com — the base address
(
Site:Business:BaseAddress, elseAddress) is geocoded once and cached for a week, the visit address is geocoded (or the job's municipality centre is used when the address is just the municipality), androuting/routewithrouteType=car_fastgives the length, rounded to whole km. „Přepočítat vzdálenost“ forces it. Without a Mapy.com key the distance stays as typed and the save shows a quiet note. - PDF text (
Pdf/PdfText.cs): QuestPDF draws only with the shipped Barlow faces and, since 2026.9, a missing glyph aborts the document. Every text in the quote and permit PDFs passesPdfText.Safeat render time: coverage is read from the TTF files (all weights), and characters they lack become readable stand-ins —⌀→Ø,→→->,✓→ano, the narrow no-break space → a space — or are dropped. Barlow lacks⌀ ∅ → ← ↗ ✓and U+202F; it has the Czech letters,– — „ “ × ≥ ≤ ½ ° ² ³ … · €.
Price quotes
A job has price quotes (Quotes/, table quotes, schema 9): /admin/zakazka/{job}/nabidka prefills the customer, the
place and the next number of the year (2026-001); items come from the price list (the felling ladder and the cards)
through a picker or are typed in, with prices including VAT like the price list. …/nabidka/{id}/pdf renders an
A4 PDF (QuestPDF) with the logo (Templates/logo-bouda-kaci.webp), supplier with IČO and DIČ (Site:Business),
customer, items, the tax base and VAT worked out from the gross total, the note and the validity. Tomáš sends it himself.
Felling permit applications
/admin/povoleni keeps applications Tomáš prepares for customers: owner, parcel, trees, reasons, photos and the
share link of the TreeMap project. /admin/povoleni/{id}/pdf renders one PDF (QuestPDF, Community licence) with the
application (§ 8 of Act 114/1992, § 4 of Decree 189/2013), a QR code of the TreeMap link (QRCoder), the power of
attorney to sign and the photos; empty fields print as dotted lines. The typeface is Barlow from
Templates/fonts (OFL). Tomáš's details as the representative come from Site:Business in configuration. The
applications hold personal data of the owner (name, date of birth, address) — delete them once the permit is settled.
When the cadastral area and the parcel number(s) are filled in, the PDF also gets an excerpt of the cadastral map
(Permits/CadastralMap.cs): the area and the parcel outlines come from the ČÚZK RÚIAN map server
(ags.cuzk.gov.cz/arcgis/rest/services/RUIAN/MapServer, layers 7 and 5), the map image from the cadastral WMS
(services.cuzk.gov.cz/wms/wms.asp, layer KN, EPSG:5514); the parcels are drawn in red with SkiaSharp. No key is
needed. The excerpt is cached for an hour; /admin/povoleni/{id}/katastr.png shows it in the admin. When ČÚZK does
not answer or the parcel is not found, the PDF is printed without it.
When the application has a TreeMap share link, Permits/TreeMapClient.cs reads the project through TreeMap's public
share API (GET /api/shares/{token}, no sign-in) and the trees within 300 m of the parcel are drawn as numbered dots
with a legend. GPS positions are converted to S-JTSK in Permits/Sjtsk.cs (Helmert + Křovák, EPSG method 9819).
The button „Načíst stromy a fotky z TreeMap“ saves the form, replaces the tree rows with the trees of the share and
downloads their photos (the tree photo and inspection photos, {photoBase}/{photoId}) into the permit photos; each
imported photo remembers its origin (permit_photos.source = treemap:<id>, schema 7), so loading again adds only new ones.
All mail of boudakaci.cz runs through Resend; Tomáš's own mailbox stays
email@tomasbouda.cz (WEDOS), read in Gmail.
| Direction | How |
|---|---|
| Inquiry → Tomáš | The site sends it from web@boudakaci.cz to Site:Resend:To, reply-to = the customer's e-mail. |
| Confirmation → customer | From web@boudakaci.cz; reply-to poptavka@boudakaci.cz once receiving is on, otherwise Site:Resend:To. |
Mail to …@boudakaci.cz → Tomáš |
MX of boudakaci.cz points to Resend (inbound-smtp.eu-west-1.amazonaws.com). Resend calls the webhook POST /api/resend/inbound; the site fetches the e-mail and attachments and forwards them to Site:Resend:To as „[boudakaci.cz] …“, reply-to = the original sender. |
| Tomáš → customer | Gmail sends as tomas@boudakaci.cz through Resend's SMTP, so the customer sees only boudakaci.cz. |
There is no mailbox at boudakaci.cz: every address of the domain ends up in the forward, so tomas@, poptavka@
and any other address work alike.
How it is set up
- Resend → Domains: boudakaci.cz verified for sending (SPF/DKIM records in DNS,
send.boudakaci.czMX for bounces). - Resend → Emails → Receiving: receiving on for boudakaci.cz; the root MX record is Resend's.
- Resend → Webhooks: endpoint
https://boudakaci.cz/api/resend/inbound, eventemail.received. Its signing secret (whsec_…) isRESEND_WEBHOOK_SECRETin the Azure DevOps groupbouda-kaci-deployment(→Site:Resend:WebhookSecret). The endpoint checks the Svix signature and refuses deliveries older than 5 minutes. - Resend → API Keys:
- the site's key (
RESEND_API_KEY) has full access — reading received mail needs it, a sending-only key gets 401/403 from the receiving API; - Gmail has its own sending-only key for boudakaci.cz, so it can be revoked without touching the site.
- the site's key (
- Gmail → Settings → Accounts → Send mail as → Add another address:
Tomáš Bouda <tomas@boudakaci.cz>, SMTP serversmtp.resend.com, port 465 (SSL) or 587 (TLS), userresend, password = Gmail's Resend key. Gmail's verification code sent totomas@boudakaci.czarrives through the forward. When answering a forwarded e-mail, picktomas@boudakaci.czin the From field — Gmail offersemail@tomasbouda.czby default, because that is where the forward was delivered.
/health shows the state in the entry inbound. A forward that fails is logged (Admin → Logy) and the e-mail stays
in Resend under Emails → Receiving; a webhook delivered twice is forwarded once. Mail coming from the site's own
sender address is not forwarded, so a bounce cannot loop.
Measuring inquiries
script.js sends GA4 events once the visitor has accepted analytics: generate_lead for a sent inquiry,
click_phone and click_whatsapp for the contact links (with link_location). Mark them as key events in GA
(Admin → Events) to count them as conversions.
Map and jobs
- District boundaries:
wwwroot/data/okresy.geojson— Plzeňský kraj, its seven districts and the nine neighbouring ones (Cheb, Sokolov, Karlovy Vary, Rakovník, Beroun, Příbram, Strakonice, Písek, Prachatice) from ČÚZK INSPIRE Územní správní jednotky (CC BY 4.0), simplified to about 100 m (148 kB). - Which districts are served is set in the admin (Okresy) and stored in the database; the map draws the districts of the region always and a neighbouring one only when it is served.
- Photos belong to a job. The admin page shrinks them in the browser (which also turns HEIC into JPEG and
applies the EXIF rotation) and the server writes a 3:4 thumbnail in two sizes and the whole photo for the
lightbox, as WebP and JPEG, to
{DataDirectory}/photosunder a random name;/media/…serves them. The gallery shows published jobs with at least one photo. - A job is entered with a municipality picked from the Mapy.com search, so its pin is always the centre of the village, never a house; the public API rounds it once more. Only published jobs are shown.
- Mapy.com asks for its logo and attribution on the map;
mapa.jsadds both. - Jobs have a status: every inquiry from the contact form is stored as a job in Poptávka (
InquiryJobs) with the customer's name, contact, message and photos, and the e-mail to Tomáš links to it. Tomáš accepts or rejects it, adds his own notes and photos to an accepted job and marks it done, which publishes it. Only a done, published job reaches the map, the gallery and the town pages, and only its photos marked Na webu — the customer's photos start private. The customer's details never leave the admin. - A job can start a felling permit application (
permits.job_id): office, owner and contact are filled in from the job and its photos are copied (new files, so deleting one side never breaks the other).
Deployment
Every push to main runs azure-pipelines.yml: tests, Docker image, smoke test, then deploy/deploy-vps.sh
on the VPS over SSH as codexdeploy — the same scheme as the app store and Arbo. The script picks a free
port (5184–5220, remembered in /opt/boudakaci/port), keeps the data in /opt/boudakaci/data, renders the
nginx site, obtains a Let's Encrypt certificate for every name in vpsDomains and only passes a release whose
/health answers Healthy.
Variable group bouda-kaci-deployment (Azure DevOps → Pipelines → Library):
| Variable | Secret | What |
|---|---|---|
VPS_SSH_PRIVATE_KEY_B64 |
yes | codexdeploy's key, base64 — the same value as in the other VPS apps' groups |
MAPY_API_KEY |
yes | Mapy.com key (developer.mapy.com → API keys); TreeMap and SrealityArchivator already have one |
ADMIN_PASSWORD |
yes | password of /admin |
RESEND_API_KEY |
yes | the Resend key from the old Azure Static Web App settings |
RESEND_FROM, RESEND_TO |
no | optional; without them the defaults in appsettings.json apply (Bouda kácí <web@boudakaci.cz> → email@tomasbouda.cz) |
The GA4 ID comes from the web-analytics group. A secret left out of a run keeps the value already deployed.
Moving the domain (Přechod domény)
Until boudakaci.cz points at the VPS, the pipeline deploys to boudakaci.tomlabs.xyz only (the
*.tomlabs.xyz wildcard already points there; the staging site sends X-Robots-Tag: noindex). To move:
- Check the staging site, sign in to
/admin, add a job. - At WEDOS change
boudakaci.czandwww.boudakaci.cztoA 37.205.12.163andAAAA 2a03:3b40:fe:764::1. Keep the TXT records (Search Console verification, Resend SPF/DKIM). - When the names resolve to the VPS, set
vpsDomainsinazure-pipelines.ymltoboudakaci.cz www.boudakaci.cz boudakaci.tomlabs.xyz,healthPathto/health, and push. The deploy extends the certificate to the new names. - Delete the Azure Static Web App.
Images and fonts
Images in assets/ are derived files; the masters are in originals/. Photos have the crop baked in to
match the CSS (object-fit: cover), so no pixels are downloaded that are never shown; when an aspect ratio
changes in CSS, regenerate the images. Variants are name-<width>.{avif,webp,jpg}; <picture> offers AVIF,
WebP and JPEG as the fallback. They are generated with ffmpeg (or sharp, installed outside the repo).
Assets are cached for 30 days and fonts for a year, and file names carry no content hash: when replacing a photo, give it a new name, otherwise visitors keep the old one for a month.
The logo master is originals/logo-bouda-kaci.webp (the whole lockup with the name). The mark on the site is only its
tree with the climber: assets/znak-96.png is transparent (originals/logo-strom-pruhledne.png, made by flattening the
master onto white and keying the white out, so no dark fringe shows) and turns white on dark backgrounds through CSS
(filter: brightness(0) invert(1)). The browser and phone icons znak-16/32/180/512.png sit on a solid light
square (originals/logo-strom.png) so they stay visible on dark tab bars. Icons have new file names on every change
— they are cached for 30 days. assets/favicon-32.png stays as a copy because an old release redirected
/favicon.ico to it permanently.
The share image assets/og-image.jpg (1200×630, what Facebook and WhatsApp show) is rendered from
tools/og-image/og-image.html (the site's fonts, the mark and originals/tomas-bouda-strom.jpg) with headless
Edge; the command is in the file. The pages link it with a ?v= suffix — raise it when the image changes, because
Facebook and WhatsApp cache the image by URL.
The fonts are Barlow (text) and Barlow Semi Condensed (headings), self-hosted.
cd src/BoudaKaci.Web/wwwroot && node ../../../tools/fetch-fonts.mjs downloads them from Google Fonts and
prints the @font-face block for styles.css. The *-latin-ext.woff2 variants are subset to Czech and Slovak
letters with subset-font (npm install --no-save --no-package-lock subset-font, outside the repo); the
*-latin.woff2 ones stay complete because they also cover what a visitor types into the form. Barlow is used
up to weight 700; when the weights change, update FAMILIES in the script and the preload in index.html.
print/vizitka.pdf is the business card (85 × 55 mm, 3 mm bleed, front and back) with a QR code to
https://boudakaci.cz/recenze — the site redirects that to the Google review form, so a printed card survives a change
of Google's link. It is rendered from tools/print/vizitka.html with headless Edge (the command is in the file); the HTML embeds its
fonts, the mark and the QR code, so it shows the same in any browser or preview;
tools/print/qr-recenze.svg is the QR code. Opened in a browser, the HTML shows both sides with the bleed tinted red
and the trim line dashed.
print/letak.pdf is the A5 leaflet (148 × 210 mm, 3 mm bleed, one side) for letterboxes and noticeboards, from
tools/print/letak.html in the same way; its QR code (tools/print/qr-web.svg) leads to boudakaci.cz. It carries no
prices on purpose — they change in the admin, a printed leaflet does not.
print/letak-a6.pdf is the same leaflet shortened for A6 (105 × 148 mm, 3 mm bleed), from tools/print/letak-a6.html:
service names only and fewer points, so the smallest text stays at 7.5 pt.
print/listek-prochazka.pdf is an A6 note (3 mm bleed) for the letterbox of a house whose tree Tomáš noticed on a walk:
ticked boxes for what is visible from the street (dry or hanging branch, fungi, crack, lean…), lines to write which tree,
and a plain statement that it is an observation, not an expert assessment. Its QR code opens the contact form with
utm_source=prochazka, which JobSource.FromAttribution turns into the source „Lísteček z procházky“ (walk), so the
statistics show what the walks bring. Print it on uncoated paper so a pen writes on it.
Round stickers for the printer's template (sheet 64 × 64 mm, safety circle about Ø 60 mm): print/samolepka.pdf has the
original logo on white, print/samolepka-tmava.pdf the site's colours — the logo in the cream #f3f1e9 (originals/logo-bouda-kaci-kremova.png) on the dark green
(tools/print/samolepka.html, tools/print/samolepka-tmava.html). The background fills the whole sheet and the logo
is 54 mm wide, so its widest part (the orange rules) stays 2 mm within the safety circle — the logo keeps its own
lettering; the site's fonts are for text.
Stickers 60 × 60 mm with a transparent background, to cut along the shape of the logo (or print on clear foil with a
white underprint): print/samolepka-pruhledna.pdf has the original logo, which already has a transparent background,
for light surfaces; print/samolepka-helma-bila.pdf and print/samolepka-helma-limetka.pdf have a light logo for
dark surfaces such as Tomáš's dark green helmet — the dark green turned cream white or the site's lime, the orange
kept (originals/logo-bouda-kaci-svetle.png, originals/logo-bouda-kaci-limetka.png, made by tools/logo-recolor;
the command is in its Program.cs). print/samolepka-helma-nahled.png compares the three on dark green.
Content still to fill in
The sections #ceny (prices) and #reference (reviews) in index.html have a finished design but placeholder
content, and therefore the hidden attribute. tools/check-placeholders.mjs fails the build if one of them is
shown while it still contains TODO. Only real reviews may ever go into them or into the structured data —
invented reviews break Google's rules.
Changelog
Changelog
1.24.1 — 2026-10-21
- Changed: the map credits Leaflet without the flag icon.
- Changed: the map lists „Vybrané hotové zakázky“.
- Added: „Web“ in the admin menu leads back to the website.
- Changed: the photo in „Kdo přijede“ has no caption over it.
1.24.0 — 2026-10-20
- Added: the job, the site visit and the quote keep the customer's full address and an optional IČO; „Načíst z ARES“ fills the name and address of a company or municipality.
- Added: the permit application and power of attorney print „Název“ and IČO for a company or municipality instead of the date of birth.
- Added: a quote is „Rozpracovaná“ or „Odeslaná“; existing quotes start as „Rozpracovaná“.
- Changed: edits now carry over into drafts — the customer from the job or visit into draft quotes and permit applications, the visit's trees and prices into the draft quote and permit it started, and the quote's place back into the visit. Hand edits in a draft are kept; sent quotes never change.
1.23.6 — 2026-10-20
- Changed: new quotes get a number like „2026-4821“ instead of a running „2026-001“, so a customer cannot tell how many quotes went out before; older quotes keep their numbers.
- Added: a quote cannot be saved with the number of another quote.
1.23.5 — 2026-10-18
- Added: „Povolení“ in the main menu leads to the felling-permit guide.
- Changed: „Co nedělám a proč“ links the arborist catalogue for expert assessments.
1.23.4 — 2026-10-18
- Added: the price list has a collapsible „Co nedělám a proč“: trees by power lines (with the ČEZ Distribuce line), expert assessments with instruments, topping and stump grinding.
1.23.3 — 2026-10-18
- Added: the photo field in the inquiry form says which photos help most: the whole tree, the trunk at the ground and what is under the tree.
1.23.2 — 2026-10-18
- Added: the price list has a collapsible „Kde můžete ušetřit“ (keep the wood, clear the branches yourself, several trees at once) and says the stump is cut low to the ground.
1.23.1 — 2026-10-14
- Changed: the phone number and e-mail are no longer in the pages for robots to harvest; a visitor sees them (and the call, WhatsApp and e-mail links work) as soon as they move the mouse or touch the page.
1.23.0 — 2026-10-12
- Added: a printable A6 note „Všiml jsem si u vašeho stromu…“ for the letterbox of a house with a tree that needs attention, with a QR code to the contact form.
- Added: the customer source „Lísteček z procházky“; an inquiry that came through the note's QR code gets it automatically.
1.22.4 — 2026-10-10
- Changed: the home page heading says what and where — "Kácení a prořezy stromů, Plzeň a okolí" instead of "Péče o stromy. Bezpečně a s láskou".
- Added: the full address (Lišice 76, 334 01 Dolní Lukavice) in the contact block and in the business data for search engines, the same as on Firmy.cz.
1.22.3 — 2026-10-09
- Fixed: the felling-permit pages no longer say that a diseased or dying tree only needs a notification. Following the Ministry of the Environment (methodical instruction, Věstník MŽP 1/2020, point 2.9, and its 2020 reply), the notification is for exceptional cases where an infested tree seriously threatens the trees around it; a dry tree alone needs a permit. The source is linked.
- Fixed: ground felling had two lowest prices (350 Kč in the table, 500 Kč on the card); the table row "do 20 cm" is now 500 Kč, and the price page and the home page say what the price includes.
- Changed: free transport is limited to 30 km from Plzeň (price page, home page, town pages, the article on felling prices).
- Changed: real availability, every day 7–20 h, in the contact block and in the data for search engines (it said 00:00–23:59).
- Changed: the cookie banner is a compact bottom sheet on phones, with Accept and Decline side by side at the same size (tomlabs-consent 1.3.0), and the phone bar (Zavolat · WhatsApp · Poptávka) moves up above it instead of being covered.
1.22.2 — 2026-10-08
- Changed: TreeMap and Stromopedie have their own addresses — the footer of every page links treemap.cz and stromopedie.cz, and the admin opens and reads TreeMap at treemap.cz. Share links on the old treemap.tomlabs.xyz address keep working: they are accepted and saved (and those already saved on visits and permit applications are moved) on the new address with the same token.
1.22.1 — 2026-10-08
- Fixed: a quote whose item mentioned the trunk diameter („kmen ⌀ 30 cm“) could not be downloaded as PDF („Něco se pokazilo“). The calculator now writes „kmen o průměru 30 cm“, and both PDFs (quotes and permit applications) replace any character the print font lacks with a readable one, so quotes saved earlier print as well.
- Fixed: the municipality search on a job offers villages that are parts of a municipality (e.g. Horušany, part of Nýřany), prefers places around Plzeň and lists those in Plzeňský kraj first; each suggestion shows the municipality it belongs to, the district and the region.
- Changed: a new visit starts with the job's municipality as its address, and the distance is worked out by car from the base (Lišice) through Mapy.com when the visit is saved with the distance empty or with a new address; „Přepočítat vzdálenost“ works it out again. A distance typed by hand is kept.
1.22.0 — 2026-10-07
- Added: „Odkud zákazník přišel“ on a job — Google, Seznam / Firmy.cz, Bazoš, web přímo, doporučení, obec / úřad, Facebook, partner, stálý zákazník, jiné — with a note; shown as a chip in the job list, which can be filtered by source.
- Added: an inquiry from the contact form gets its source automatically from the campaign parameters and the referring site of the visit (the page remembers them for the session), together with non-identifying details of the visit — device, browser, system, language, first page, referrer and campaign — shown collapsed as „Technické údaje poptávky“ on the job. No address and no raw browser string are stored.
- Added: „Statistiky“ in the admin — inquiries, accepted, done and rejected jobs per month (with a bar for each of the last 12 months) and per year, revenue from the latest quote of done jobs, hours on site from the visits and the realised crowns per hour, the sources with their conversion, the devices of inquiries, and a CSV export of the jobs.
1.21.0 — 2026-10-07
- Added: the round trip with TreeMap — „Otevřít v TreeMapu“ on a visit without a project opens TreeMap with a new project for the job (named after the job, under the configured „Zakázky“ project); „Zpět do administrace“ in TreeMap brings you back to the visit with the project connected and its trees loaded. No more copying of share links; a visit with a project keeps „Otevřít v TreeMapu“ for it and offers „Založit nový projekt v TreeMapu“ as well.
- Added: settings
Site:TreeMap:BaseUrl(defaulthttps://treemap.tomlabs.xyz) andSite:TreeMap:JobsProject(defaultZakázky).
1.20.2 — 2026-10-07
- Changed: the empty tree card on the visit page is folded into one „+ Přidat strom“ row; tapping it opens the card and puts the cursor into the species field. The card stays open for the first tree of a visit and when its fields have an error.
1.20.1 — 2026-10-07
- Fixed: a tree loaded from TreeMap and deleted from the visit with „Smazat strom“ stays deleted — reopening the visit no longer adds it back. The deleted trees can be brought back by ticking „Načíst znovu i smazané stromy“ next to „Načíst stromy z TreeMapu“.
1.20.0 — 2026-10-07
- Added: „Obhlídka“ on a job in the admin — the site visit recorded on the phone at the tree: date, address, distance, access (trailer or carrying), wires near the tree (power lines show a red warning), and one card per tree with species, height, circumference at 130 cm (the diameter is shown), the fruit-tree exemption, the kind of work, what is near the tree, the state of the wood, what happens to the wood and brush, the stump, the hours and a helper. A tree over 80 cm circumference that is to be felled is flagged „Potřebuje povolení“.
- Added: a price calculator under the visit — every proposed line explains how it was worked out (ladder row by diameter, risky felling as hours plus a risk surcharge, pruning card, cleanup hours, carrying, hauling, travel beyond the included kilometres), with the total and a check that every hour on site earns at least the hourly rate.
- Added: „Vytvořit nabídku z obhlídky“ opens a new quote prefilled with those lines, and „Založit žádost o povolení“ starts a permit application with the trees that need one and the customer as owner.
- Added: the calculator's parameters in Ceny → „Kalkulačka obhlídky“: hourly rate, helper rate, included kilometres and the rate beyond them, surcharges for things near the tree and for bad wood, carrying time, rounding, and the default hours per kind of work and tree height.
- Added: trees of a visit come from TreeMap — paste the project's share link, „Načíst stromy z TreeMapu“ (or just reopening the page) adds the shared trees with species, circumference, height and the kind of work from the proposed measure, refreshes the measurements of trees loaded before and never touches what was typed about the price; „Otevřít v TreeMapu“ opens the project.
1.19.2 — 2026-10-07
- Fixed: www.boudakaci.cz redirects permanently to boudakaci.cz, so search engines see one address.
1.19.1 — 2026-10-06
- Removed: the Instagram and YouTube rows from the contact details; the links stay in the footer.
1.19.0 — 2026-10-06
- Added: links to Instagram (@boudakaci) and the YouTube channel in the footer of every page and in the contact details; search engines get them as the business profiles too.
- Fixed: the menu folds into the menu button on tablets instead of wrapping its links.
1.18.1 — 2026-10-05
- Changed: shorter wording about hanging branches in the article on trees after a storm.
1.18.0 — 2026-10-05
- Added: „Rady“ — advice articles: how to spot a dangerous tree, what to do with a tree after a storm, and what felling a tree costs. Linked from the menu and listed in the sitemap.
- Added: search engines get the FAQ of the home page and of every article as structured data, and the area served follows the districts set in the admin.
- Fixed: the grey secondary text has enough contrast to read comfortably.
- Fixed: the gallery no longer jumps while the jobs load.
1.17.1 — 2026-10-05
- Removed: the optional municipality and kind of work from the inquiry form again — the form stays as short as possible.
1.17.0 — 2026-10-05
- Added: two optional fields in the inquiry form — the municipality (suggested as you type) and the kind of work.
1.16.1 — 2026-10-04
- Changed: the draft of the inquiry is kept quietly, without notes or a delete button.
- Changed: the site shows tomas@boudakaci.cz as the e-mail address everywhere.
1.16.0 — 2026-10-04
- Added: the inquiry form keeps a draft in the visitor's browser — name, contact, message and photos come back after closing or reloading the page; it is deleted after sending, after 30 days or with „Smazat koncept“.
1.15.0 — 2026-10-04
- Changed: no more dozing in the „Kdy kácet“ scene — while birds nest, Tomáš checks the nest with binoculars; in late summer and autumn he measures the trunk at 130 cm for the permit.
1.14.1 — 2026-10-04
- Changed: the bird on „Kdy kácet“ says just „Ještě ne!“.
1.14.0 — 2026-10-04
- Added: in December the tree on „Kdy kácet“ is a Christmas tree with baubles, twinkling lights and a star.
- Changed: a nicer Tomáš in the scene — dark green helmet with ear defenders and visor, beard, harness, chainsaw trousers; resting with the helmet over his eyes and the saw by the stump.
1.13.1 — 2026-10-04
- Changed: shorter texts in the seasonal scene on „Kdy kácet“.
1.13.0 — 2026-10-04
- Added: a page „Kdy kácet“ — the months for felling, why the dormant season is best, what applies while birds nest, what can be done all year, and a link to the permit guide.
- Added: a little seasonal scene on that page: in the dormant season Tomáš saws a bare tree, while birds nest a bird sings on the tree and Tomáš dozes; click a month to see it, click the figures to hear them.
1.12.0 — 2026-10-01
- Added: price quotes for a job in the admin — items picked from the price list or typed in, prices including VAT, and a PDF with the logo, IČO and DIČ, the tax base, VAT and total, ready to send to the customer.
- Changed: documents show tomas@boudakaci.cz as the contact address.
1.11.1 — 2026-09-30
- Changed: the footer link to the arborist directory (Arboristé) points to its new address katalogarboristu.cz.
1.11.0 — 2026-09-29
- Added: e-mails sent to any address at boudakaci.cz are forwarded to Tomáš with their attachments; answering the forward goes straight to the sender.
- Changed: once receiving is set up, customers reply to the confirmation e-mail at poptavka@boudakaci.cz, which is forwarded the same way.
1.10.1 — 2026-09-29
- Fixed: the links to the other tree projects in the footer are underlined as a whole and keep a 40 px touch target.
1.10.0 — 2026-09-29
- Added: every inquiry from the website becomes a job in the admin (Poptávka) with the customer's name, contact, message and photos; the e-mail about it links there.
- Added: jobs have a status — Poptávka, Přijatá, Hotová, Odmítnutá — with tabs in the job list and buttons to accept, reject and finish; private notes for address, price and date.
- Changed: only a finished, published job appears on the map, in the gallery and on the town pages, and only with photos marked „Na webu“; the customer's photos start private.
- Added: a felling permit application can be prepared straight from a job — office, customer and copies of the photos are filled in.
- Added: the footer of every public page links to Tomáš's other tree projects — Stromopedie (tree encyclopedia), TreeMap (tree mapping and assessment) and Arboristé (arborist directory).
1.9.5 — 2026-09-29
- Fixed: the inquiry form answers right away — the confirmation e-mail or text goes out in the background, so a slow SMS gateway no longer keeps the customer waiting; SMSbrána gets up to 90 s to answer and a late answer is not reported as a failure.
1.9.4 — 2026-09-29
- Fixed: spaces or line breaks around the SMSbrána login and password no longer break the sign-in; a refused text logs the login and time used.
1.9.3 — 2026-09-29
- Changed: the confirmation text reads „…ozvu se do 24 hodin. Na tuto SMS neodpovídejte, v případě potřeby volejte 605 870 521. S pozdravem Tomáš Bouda, boudakaci.cz“ (one SMS).
1.9.2 — 2026-09-29
- Changed: the confirmation text no longer asks the customer not to reply — SMSbrána forwards replies to Tomáš's e-mail.
1.9.1 — 2026-09-29
- Changed: confirmation texts go through SMSbrána.cz instead of a phone app; the text tells the customer to call 605 870 521, because it comes from the gateway's number.
1.9.0 — 2026-09-29
- Added: a customer who leaves a mobile number in the inquiry gets a confirmation text, sent from Tomáš's own phone through SMS Gateway for Android (active once the gateway credentials are set in the pipeline).
1.8.2 — 2026-09-29
- Added: boudakaci.cz is listed in the TomLabs app store (apps.tomlabs.cz); every deploy refreshes the catalogue entry.
1.8.1 — 2026-09-29
- Changed: a new share image for Facebook and WhatsApp — Tomáš in the cherry crown with the new logo, „Rizikové kácení stromů v Plzeňském kraji“ and the contact.
1.8.0 — 2026-09-29
- Added: „Načíst stromy a fotky z TreeMap“ in a permit application fills the list of trees (Czech and scientific name, circumference) from the TreeMap share link and adds the tree and inspection photos to the photo documentation; loading again adds only new photos.
- Fixed: the confirmation before deleting a permit application was not shown.
1.7.2 — 2026-09-29
- Fixed: the permit map legend names trees in Czech with the scientific name (e.g. „Zerav západní (Thuja occidentalis)“) instead of TreeMap's internal codes such as „jine“.
1.7.1 — 2026-09-29
- Changed: a tree measured in TreeMap by its diameter only gets its circumference computed in the permit map legend (the permit rules count the circumference).
1.7.0 — 2026-09-29
- Added: when the application has a TreeMap share link, the trees of that project are drawn into the cadastral map excerpt as numbered dots, with a legend (species, circumference, tag) under the map.
1.6.0 — 2026-09-29
- Added: the permit application PDF includes an excerpt of the cadastral map (ČÚZK) with the parcel highlighted, its kind of land and area; the admin shows the excerpt as a preview. Several parcels are separated by commas, building plots are written „st. 12“.
- Changed: the FAQ answer „Jak rychle přijedete?“ says that when a tree threatens a house or people, Tomáš comes as soon as possible.
1.5.1 — 2026-09-29
- Changed: the permit application and the power of attorney carry Tomáš's full address.
1.5.0 — 2026-09-29
- Added: felling permit applications in the admin (Povolení) — owner, parcel, trees, reasons, photos and a TreeMap link; one PDF for the municipal office with the application, a QR code of the TreeMap project, the power of attorney to sign and the photos.
1.4.2 — 2026-09-29
- Changed: the confirmation e-mail is signed „S pozdravem / Tomáš Bouda / boudakaci.cz“.
1.4.1 — 2026-09-29
- Changed: the confirmation e-mail to the customer ends with „S pozdravem“.
1.4.0 — 2026-09-29
- Added: the page Ceník (cenik.html) — felling prices by trunk diameter, from the ground and in parts, edited in the admin (Ceny); the other price cards and what the price depends on.
- Added: the page Povolení ke kácení — when a permit is needed, how to apply, what happens next, sick trees and emergencies; the permit is arranged for free.
- Added: a page for every municipality with published work (/kaceni-stromu/…) with its jobs, photos and nearby places; the map links to them; the sitemap is generated with them.
- Added: customers who leave an e-mail get a confirmation of their inquiry (at most two a day per address); text messages wait for an SMS gateway.
- Added: GA4 events for a sent inquiry and for clicks on the phone and WhatsApp (after consent only).
1.3.2 — 2026-09-29
- Added: the felling permit is arranged for the customer free of charge (FAQ and the process).
- Added: no minimum order and no transport charge, stated with the prices and in the FAQ.
- Added: "Nejdřív zkusím strom zachránit" at the top of the services.
1.3.1 — 2026-09-27
- Changed: new logo — the tree with the climber from the Bouda kácí logo, in the header (dark) and the footer and admin (white), and as the browser and phone icon.
1.3.0 — 2026-09-27
- Added: the price list is edited in the admin (Ceny) — name, "od", amount, unit, description, order, visibility; add and delete items. The home page shows the change at once.
- Changed: the admin writes Czech letters as they are in the page source instead of numeric entities.
1.2.0 — 2026-09-26
- Added: indicative prices (felling from the ground, risky felling in parts, pruning, clean-up per hour), including VAT; free inspection.
- Added: "Kdy je nejlepší kácet" — a month bar with the dormant season and bird nesting, the current month highlighted.
- Added: frequently asked questions, including when a felling permit is needed (Act 114/1992, Decree 189/2013).
- Added: /recenze, a short link to the Google review form; "Napsat recenzi" in the footer.
- Changed: texts reviewed — no work near power lines, pruning without topping, forest work only as marked by the owner, first person throughout, work also at weekends.
- Changed: the gallery lists only jobs from the admin; job kind "Prořez a odlehčení koruny" is now "Prořez".
1.1.0 — 2026-09-25
- Added: photos of a job in the admin — upload straight from the phone, captions, order, delete. The server makes the gallery thumbnails and the large view itself.
- Changed: the gallery page lists the published jobs that have photos, each with its place, month and note; the map popup shows the first photos and links to them.
- Added: served districts are edited in the admin (Okresy), including the districts around Plzeňský kraj.
1.0.0 — 2026-09-25
- Added: the page Kde pracuji (
mapa.html) — a map of the districts of Plzeňský kraj the business serves and of the municipalities with finished work, on Mapy.com tiles. - Added: an administration at
/adminto enter finished jobs for the map (municipality picked from the Mapy.com search, so the map shows the village, never an address; drafts stay off the map). - Added: the gallery page with photos from the cherry-tree job, opening in a lightbox.
- Added: the version in the footer and
GET /healthfor monitoring. - Changed: the site moves from Azure Static Web Apps to the VPS as a small ASP.NET Core application; the contact form works the same way.