Biogard
My first project: a pest-control company's site, rebuilt self-hosted
- Role
- Solo, directing agent fleets
- Status
- Live
- Source
- Private repository
- Stack
- Next.js 16React 19TypeScriptTailwind CSS 4PrismaPostgreSQL 17Docker ComposeCaddygVisorUmamiResendTailscale

In numbers
100
mobile PageSpeed score in all four categories (home page)
1.5s
mobile LCP, down from 2.7 s
122
pages crawled with 0 problems
38
hand-drawn SVGs: 27 pests, 9 places, 2 scenes
55
routes in two languages, each mapped to one primary keyword so pages do not compete
13
species in the hidden bug game, with a golden fly and a Cockroach King boss
The problem
Biogard is the first thing I built. My oldest saved chat on it is about a React app that would not start, and a hidden fly minigame was already in the plan early on. After that the site moved to Next.js 14 on Netlify with a Neon database. By the time of the rebuild it showed its age. The database's free tier ran out of quota and blanked the live blog (post pages returned 500). The icons were auto-traced and the FAQ had an 'Ask the AI' button.
The approach
I wrote a one-sentence brief: rebuild it to today's standard, keep the information and every URL, no AI look, hand-drawn illustrations, both languages. The agent wrote the shell first (i18n, routing proxy, SEO helpers, design tokens, header and footer), then fanned out eight agents in parallel across pages, blog, legal, APIs, illustration and SEO. A site crawler written during the fan-out became the quality gate. Production DNS then moved to the new server.

How it works
A URL contract
Bulgarian pages are unprefixed and English lives under /en; a proxy rewrites both into app/[lang] and 308-redirects old /bg/ paths. The build guide forbids renaming a slug, because the slugs are the indexed URLs. At cutover the crawler reported 122 pages and 0 problems.
Hand-drawn SVG
27 pest icons, 9 places and 2 scenes, drawn on a 64 by 64 grid with currentColor strokes, replaced the auto-traced set. I cut a generated hero video and put a drawn cutaway of a house in its place, with pests turning back at a red barrier. The copy rules ban em dashes and stock AI phrasing.
Isolated on a shared host
Docker Compose runs Caddy, Next.js and Postgres 17, and only Caddy publishes ports. Containers are read-only with all capabilities dropped, no-new-privileges and CPU, memory and PID limits; images are pinned by digest, and gVisor is available for the internet-facing services. An egress firewall chain stops the containers from opening connections into the tailnet, cloud metadata or private ranges. Releases are timestamped directories behind a current symlink with a rollback script, each deploy runs smoke tests that check host isolation, and the database is dumped nightly.
SEO and compliance as a phase
The SEO plan maps 55 routes in two languages to one primary keyword each, so pages do not compete for the same query, with titles of at most 60 characters, descriptions of 140 to 160, llms.txt and four sitemaps. The site publishes no prices, since each treatment is quoted individually. Claims that biocides are safe, non-toxic or eco were removed to comply with Article 72 of the EU Biocidal Products Regulation.
A bug game the site does not mention
Off by default. Typing 'biogard' or the Konami code, or tapping the logo seven times, wakes 13 species including a golden fly and a Cockroach King boss; 'ddd' fumigates the page, and moths swarm between 21:00 and 05:00. Analytics count who finds it and who plays. It keeps the site personal without getting in a customer's way.

What I chose, and what lost
Chose
Self-host on an existing server, isolated by containers and an egress firewall
Over
A dedicated small server, which the runbook itself calls the safer design
I did not want a second server. The runbook records the accepted risk (code execution in the web container still needs a container escape to reach the host) and the exit: moving the site to its own box changes nothing in the app.
Chose
Google search ads
Over
Meta ads, after a test of 7 ads and 9,077 impressions
Search produced a phone call and a Viber message on its first day; the Meta test produced 0 calls. Meta was switched off.
Chose
No AI in the product: the FAQ's 'Ask the AI' button, its API route and the AI clauses in the legal pages were removed
Over
Keeping an AI answer box on the FAQ
I did not want visitors' questions sent to a model.
Outcome
Live at biogard.bg since the DNS cutover. After PR #39, Google PageSpeed Insights scored the mobile home page 100 in all four categories, with LCP down from 2.7 s to 1.5 s (the agent's own run, not repeated for this page). Self-hosted, cookieless Umami counts visitors, filtered to Bulgarian time or language after foreign bot traffic showed up. Unfinished: the Google reviews block is built and stays off until the listing has three ratings, double opt-in for the newsletter was dropped as friction and sits as dormant code, and there are only 24 unit test cases, so the crawler is the main gate. The ads method is now a written playbook in the ValoxVSL repo.
What comes next
Turn the reviews block on once the listing has three ratings.
Gallery
Hand-drawn service scene: a drone over reeds.
Hand-drawn service scene: a kitchen.
Hand-drawn service scene: a park.
Hand-drawn service scene: an apartment block.
Hand-drawn service scene: a barn and a greenhouse.
Rebuilt as version two.