Six years of continuous WooCommerce service for a Polish pet retailer — plus a production-grade AI layer on top.
E-commerce + AI integration
John Dog (johndog.pl)
2020–2026
Senior Developer · Tech Lead · AI Engineer
At a glance
Six years of weekly increments on a custom WooCommerce theme — cart, checkout, multi-variant catalog, 4-carrier logistics, content surface — then a production AI monorepo (chatbot V6 + dashboard + ranking + translator + webui) deployed next door on the same VPS.
01 — The problem
Pet retail in Poland in 2020 looked like every other mid-market e-commerce. Then it had to grow up.
WordPress + WooCommerce + a half-finished theme. Three or four shipping carriers half-integrated. A wishlist plugin that broke on logout. A checkout flow that didn’t know what to do when a customer added a 25 kg bag of dog food with curbside pickup. An admin panel where adding a new product variant required four browser tabs. The catalogue grew faster than the platform could absorb. The agency answer was always “migrate to Shopify” — which would have rebuilt the same problems behind a higher monthly bill and lost six years of SEO + custom logistics integrations that no off-the-shelf platform could match.
So the project took a different shape: continuous service. The theme was never rewritten. It was kept alive with weekly increments — a cart fix here, an Allegro label-printing module there, a Puppy Zone aggregator next month, a guest wishlist with cookie persistence after that. Every commit a small, reversible change on a live store; no big-bang migration that risks the Christmas season.
The second problem arrived in 2026: customers wanted to talk to the store. Not chat-with-a-human — chat-with-an-AI-that-knows-the-catalogue. Generic chatbot widgets bolt on a textarea and hallucinate prices in week two. Production grade needed RAG over the real Qdrant index, agent loops that don’t loop forever, BOK email escalation when the bot hits its competence wall, and GDPR-clean fonts so we don’t leak IPs to Google before the user clicks “accept cookies”. So a second repo (`johndog-ai`) joined the first — deployed to the same VPS, reading the same product catalogue, escalating to the same BOK team.
02 — The vision
Continuous over greenfield. — and AI on top, not AI instead of.
Four product principles drove six years of decisions:
- Continuous over greenfield. The theme isn’t rewritten every year — it’s kept alive with weekly increments. Every commit is a small, reversible change on a live store; no big-bang migrations that risk Christmas season. Six years in, the same theme that launched in 2020 still ships new modules every fortnight.
- WooCommerce, but never “that’s how it works”. When the standard plugin can’t model the business — multi-variant ACF linking for thousands of SKUs, guest wishlist that survives logout, 4-carrier label printing with warehouse scanner integration — we drop down to PHP + ACF + custom templates rather than bend the business to the plugin.
- AI on top, not AI instead of. The chatbot lives next to the store, not inside it — Qdrant has its own embeddings, the bot has its own SQLite, the agent has its own guardrails. If AI breaks at 3 a.m., commerce keeps running. Two repos, one platform, two blast radii.
- GDPR is a feature, not a footer. Self-hosted fonts (Oswald + Cooper Hewitt, never Google Fonts — LG München precedent), IP whitelisting on the dashboard, SMTP instead of Resend (no third-party email-API key in our header), Presidio DLP filter on every LLM input. Built in from the first commit, not bolted on after the first audit.
03 — Who it’s for
Four very different shoppers, one storefront that has to make sense to all of them.
Barbara, 45 — golden retriever owner
Busy professional, buys premium karma online twice a month. Needs trusted food that won’t trigger her dog’s allergies, fast delivery (DHL or InPost), and expert content she can read on the train. The chatbot suggests by breed + age + allergy profile.
Jakub, 32 — first-time puppy owner
Overwhelmed by choices. Lands on the dedicated Puppy Zone, reads the educational blog, the bot recommends an age-appropriate starter kit (food + collar + leash + bed). The cross-sell popup in cart upsells the matching travel carrier.
Agnieszka, 50 — small-shop B2B buyer
Sources cat food in volume for her own little pet shop. Relies on Allegro API integration for reliable logistics + invoicing. No drop-shipping drama, no minimum-order surprises — the warehouse automation just works.
Paweł, 28 — pet café owner
Curates premium JD products for his café’s retail shelf. Embeds the chatbot widget on his own café site (same Qdrant index, same agent) so visitors can ask about treats and pick them up at the counter.
04 — Two repos, one story
One platform. Two repositories. Six years apart.
The theme was already a six-year platform when the AI repo started. Both deploy to the same VPS, both watch the same product catalogue, both escalate to the same BOK team. The AI doesn’t replace any theme functionality — it sits next to it, reads the same products, hands off to humans when it hits its competence wall.
john-dog-theme
Custom WooCommerce theme — continuous service since March 2020.
- 6 years of weekly increments
- PHP / SCSS / JS — ~3 MB of bespoke code
- 44 ACF field groups
- 20+ custom
functions/modules - 4 shipping carriers integrated
- Webpack 5 + Laravel Mix build pipeline
johndog-ai
Production AI monorepo — started March 2026, five apps inside.
- Python 48% / TypeScript 24% by size
- 5 apps: chatbot widget + dashboard + ranking + translator + webui
- FastAPI + Pydantic + async SQLAlchemy
- Qdrant with 5 collections (products, knowledge, glossary…)
- Gemini 3-flash + 2.5-flash + 3.1 Pro per workload
- V1→V6 agent iterations on production traffic
05 — The architecture
Two stacks, one VPS, one product catalogue.
The WooCommerce theme is a long-lived PHP application: custom child theme, ACF field groups, 20+ functions/ modules, four shipping carriers each in their own folder. The AI layer is a Python monorepo running independently in Docker: FastAPI orchestrator, V6 agent with four guardrail layers, Qdrant vector DB with five collections, Gemini 3-flash for chat plus Gemini 3.1 Pro (1 M context) for ranking articles. Both deploy to the same Mikrus VPS behind nginx. Both read the same product catalogue. Neither knows how the other is implemented.
Theme — `john-dog-theme`
functions/ moduleswoocommerce-allegro · -dhl · -paczkomaty-inpost · -paczka-w-ruchuAI — `johndog-ai`
search_products · search_knowledge · sentiment batch · contact extractor06 — Technical challenges
Eight engineering problems from six years of weekly increments — six from the WooCommerce theme, two from the AI layer.
Four shipping carriers, four label formats, one warehouse
Problem. JD ships through Allegro (curated marketplace + API), DHL, InPost Paczkomaty (parcel lockers), and Paczka w Ruchu (corner-shop pickup). Each carrier exposes a different label format, a different printer API, a different tracking system. WooCommerce natively supports one or two; we needed all four simultaneously with real-time label printing and warehouse scanner integration.
Solution. Custom PHP modules per carrier in their own folders: woocommerce-allegro/, woocommerce-dhl/, woocommerce-paczkomaty-inpost/, woocommerce-paczka-w-ruchu/ — each with template overrides for order confirmation and label printing, each with its own checkout preview so the customer sees accurate shipping cost and ETA per carrier. Allegro API handles SKU sync, label generation and a printer queue the warehouse staff scans into. One order in WooCommerce, one label out of the right carrier, every time.
Multi-variant catalogue + guest wishlist that survives logout
Problem. Thousands of SKUs across food (by size, flavour, life stage), accessories (by size, colour, material), supplements (by weight, dose). Native WooCommerce variants get unwieldy past ~5 attributes; admin staff fought four browser tabs per new product. Separately: logged-out users lost their wishlist on every session expire, so first-time browsers walked away with no saved interest.
Solution. ACF-based linked-product variants — each “variant” is a real product linked from the parent, rendered as a variant in grids and product pages. Editors get one screen, the database gets clean joins. Guest wishlist: cookie-based pending_products store that survives logout, merges on next login (commits 4f03538, 45e6aa9, e6d748a). Wishlist icon promoted to its own slot in the mobile menu instead of buried in the account dropdown (commit 23bfa40).
Editorial layers on top of a catalogue store — Podcasty, Puppy Zone, Blog
Problem. JD isn’t just a shop — it’s an authority. The team wanted a podcast hub with seven categories, a dedicated Puppy Zone for new-pet owners, a blog with proper taxonomy, plus cross-sell logic across all of them. Each layer required custom post-type, ACF grouping, submenu restructuring and CSS sizing iterations the editorial team could live with.
Solution. Podcast submenu with 7 categories driven by SVG mask icons (15+ commits iterating icon sizing, alignment, focus state — commits abeadae, 0cb689d). Puppy Zone as a custom CPT aggregator with filtering by tag and taxonomy (commit 1d0b01d), replacing the deprecated “Book” view (2f297de, d0ac291). Coupon banner as an ACF toggle, default-on (80a3239). Editorial team can manage everything from the admin; engineering ships features.
Cross-sell that doesn’t recommend out-of-stock products
Problem. Checkout added a cross-sell popup with related products — classic conversion lever. But some recommended items were out of stock at the moment of click. Customers hit “add to cart” on a sold-out treat, got an error, lost trust in the recommendation. A bad cross-sell is worse than no cross-sell.
Solution. Stock-availability filter in the cross-sell component (commits 0491a89, 27b9994): backend query joins on stock status, popup only renders available SKUs, inline add-to-cart works without leaving the checkout flow. Out-of-stock items quietly drop from the recommendation pool; the popup never shows an empty state because the filter keeps a deep enough candidate list.
Modern CSS (:has()) without breaking old browsers
Problem. A chunk of the checkout uses CSS :has() for state-dependent layout (e.g. shipping-method-aware fields). Modern Chrome/Safari/Firefox handle it; older browsers silently fail and the checkout looks broken without throwing anything. The Wishlist button also had a z-index conflict with product overlays on mobile (commit f372daf).
Solution. CSS-only browser detection through @supports selector(:has(*)) — if the selector parses, the modern layout applies; if not, a fallback warning banner appears (commit 8fb28be). Z-index refactor for the mobile wishlist button + product overlays so they stop fighting each other. iOS zoom-trap fix (input font-size: 16px minimum) bundled in the same sprint. Graceful degradation, no JS, no polyfills.
Performance on a high-SKU catalogue — Webpack 5, Brotli, lazy-load
Problem. Thousands of products means thousands of images on category pages. Page weight ballooned; build pipeline got slow; no asset-versioning strategy meant cache invalidation was a deploy roulette.
Solution. Build pipeline upgrade to Webpack 5.105.4 + Laravel Mix 6 with compression-webpack-plugin emitting both .br (Brotli) and .gz alongside the raw CSS (commit 32ad7a5). File-versioning baked into the build (commit d973893) so every deploy cache-busts cleanly. vanilla-lazyload 19.1 for images + iframes (no jQuery dependency). Stylelint 17 config tuned to ignore the legacy BEM selectors that aren’t worth rewriting just to pass the linter. Page loads in under 3 s on 3G, assets cache for a year.
AI agent reliability — six iterations to get to V6
Problem. The chatbot runs on a live store with real customer money on the line. The naive Gemini integration had six classic failure modes: hallucinating products that aren’t in the catalogue, rewriting prices, switching language mid-response (PL→EN), dropping facts in longer turns, leaking customer data, and taking 80+ seconds to respond before nginx 504-ed the whole conversation.
Solution. Six iterations of the agent (commits bbb25fd, 19829ed, 4d6ef14, 25164f7, 8682811, a8360e0, 5f28c6e, 6ee474a, a3a8f0d). V1–V4: prompt-engineering only — failed at scale. V5: question-first agent with species-aware slot extraction (dog vs cat, puppy vs adult vs senior) plus a health-gate that blocks recommendations on serious symptoms until turn 2. V6: early-show balanced — products surfaced on turn 0 if the query is complete, with compact tool results (75K→5K tokens). Four-layer guardrails throughout: anti-prompt-injection regex, language hard lock, completeness check, and data-user-edited="1" protection. Response time: 80+s → 15–25s.
Widget RODO compliance + CSS isolation on the host site
Problem. The chatbot widget embeds on johndog.pl. Two failure modes: (1) loading Google Fonts in the widget would send the visitor’s IP to Google before RODO consent — the LG München 2022 precedent makes this an actual fine risk. (2) The widget’s CSS could leak into the host site’s modal stack; z-index conflicts made the launcher button visually fight with product overlays on mobile.
Solution. Self-hosted Oswald + Cooper Hewitt fonts reused from john-dog-theme/assets/fonts/ — same origin, zero IP leaks to Google (commit a8360e0, JD-139). Widget CSS scoped tightly with no Tailwind global scope. Z-index sandboxed to 1998/1997 so the widget always sits below the host’s modal stack at 2000. Mobile lifecycle fixes (commits bbb25fd, 19829ed): launcher auto-hides when chat opens, greeting bubble TTL 3 days via sessionStorage, expand-mode disabled below 480 px (full-screen only). Widget integrates without breaking the host; zero compliance violations.
07 — The workflow
Shopper visits site. Bot answers questions. Cart converts. BOK takes over when the bot hits its wall.
A customer lands on johndog.pl and filters the catalogue by ACF-driven attributes (food size, breed age, allergy profile). Custom checkout shows all four carriers with dynamic shipping costs — they pick Paczkomaty, the warehouse prints an InPost label automatically. In parallel: the chatbot widget streams Gemini 3-flash answers grounded in the Qdrant products_pl_v3 index. V6 agent (early-show balanced) surfaces 3–5 product recommendations on turn 0 if the query is complete; otherwise it asks a question first. If the shopper asks about a coupon code, order status or payment problem — the contact extractor pulls their email + phone via regex, the LLM writes a transcript summary (Gemini 2.5 Flash), and aiosmtplib sends a BOK e-mail with the full conversation. The dashboard team sees the conversation live, the sentiment batch runs every 30 minutes, and Request Inspector flags any anomaly that crossed an alert threshold.
08 — Feature highlights
Thousands of products and four carriers on the storefront, two AI tools behind the team login.
The public surface advertises the catalogue, multi-carrier shipping, Puppy Zone content, podcasts, expert articles and the chatbot widget. The two features below are the internal AI tooling that the JD team actually uses every day — not the public chatbot, but the back-office layer that makes the team faster.
+ Open WebUI with Presidio DLP filter
Internal AI assistant for the JD team — same Gemini backend as the public chatbot, but with the full jd360agentPrompt.md system prompt and two custom tools (search_products_v2 and JDknowlageBlog) querying the same Qdrant collections. Microsoft Presidio runs as a pre-LLM PII mask: customer emails, phone numbers, addresses get redacted before any prompt leaves the box. The team can ask about real conversations without leaking real customer data.
+ Ranking Tool powered by Gemini 3.1 Pro (1 M context)
17 preset ranking topics (best puppy food, top hypoallergenic karma, etc.). The tool scrolls the entire products_pl_v3 Qdrant collection (300+ products), enriches with the karma + przysmaki Excel collections, then runs a 2-pass selection: Gemini 3.1 Pro narrows 300 → 20, then writes a ranked Top-10 article with nutritional analysis. Sequential asyncio.Queue — one article at a time, deterministic output. Comments + regeneration workflow lets the editor refine without losing context.
09 — Stack
Two stacks, one VPS — picked for six-year longevity, not for the latest hype cycle.
Every dependency below earns its place by either being there since 2020 and never having broken, or being the cheapest correct option for the AI layer. The whole platform shares a single Mikrus host.
john-dog-themeacf-json/ field groupsfunctions/ PHP filesjohndog-aigoogle-genai)cp.10 — Results
Six years live, thousands of SKUs shipped, AI chatbot answering hundreds of conversations a day.
John Dog is in production on johndog.pl with weekly increments still shipping in 2026. Four shipping carriers integrated, multi-variant catalogue running on ACF-based linking, custom checkout handling everything from a 25 kg bag of dog food with curbside pickup to a single chew-toy to InPost Paczkomaty. The chatbot widget streams Gemini 3-flash answers grounded in the Qdrant index, the dashboard team watches conversations + sentiment in real time, and Twenty CRM runs the BOK workflow on a sibling subdomain.
11 — Engineering decisions
Seven ADRs — six years of decisions, each ruling out a more popular alternative on purpose.
Architecture Decision Records are how we agreed once and stopped re-litigating. The seven below cover the parts that look obvious from the outside and were anything but.
Continuous service over rewrite
Context. Year three. The catalogue had outgrown what the original theme was designed for, new plugins were being demanded faster than we could integrate them. The pitch from outside vendors was always “migrate to Shopify”.
Decision. Stay on the same theme, keep adding modules. Six years of weekly increments instead of one big-bang migration. Every new feature is a small, reversible change on a live store.
Consequence. Six years of SEO equity preserved, four custom carrier integrations kept, zero Christmas seasons lost to platform migration. The Shopify monthly bill never materialised.
ACF-based linked variants over native WC variants
Context. Native WooCommerce variants degrade in usability past ~5 attributes. JD’s catalogue has products with food size + flavour + life stage + breed-size + grain-free flag — native variants made the admin unusable.
Decision. Each “variant” is a separate product, linked from a parent via ACF. Listings render the parent with a variant picker driven by the link table; product pages render the actual variant.
Consequence. Editorial team gets one screen per new SKU. Database joins stay clean. WooCommerce stock + Allegro sync work per individual SKU instead of per-attribute-combination. The trade-off: more rows in the DB, no built-in attribute filtering — both handled by FiboSearch.
AI as a sibling repo, not a WooCommerce plugin
Context. The natural place for the AI layer is “inside WordPress” — a plugin, a custom post-type for conversations, PHP hooks. We considered it for about a day.
Decision. Separate repo (johndog-ai), separate Docker stack, separate database, separate failure mode. Deployed on the same VPS but in its own process. Reads the WC product catalogue through the WooCommerce REST API, not through PHP.
Consequence. If the AI breaks at 3 a.m., commerce keeps running. If WooCommerce upgrades break the theme, the chatbot doesn’t notice. Blast radius isolation by default. Two stacks to monitor, but two clean blast zones.
Gemini native SDK over OpenAI for conversation
Context. The chatbot needs cheap, fast streaming. OpenAI is the obvious default; we tested both for our actual prompt shape.
Decision. Gemini 3-flash-preview via the native google-genai SDK for chat. OpenAI text-embedding-3-small for embeddings (Qdrant compatibility). Mixed-vendor by deliberate choice.
Consequence. Lower per-turn cost than GPT-4o-mini. Better latency for streaming. OpenAI fallback is one config flag away. The trade-off: two SDKs to mock in tests — worth it.
V6 early-show over V5 question-first agent
Context. V5 asked clarifying questions before showing products. Conservative, safe, slow. Users churned mid-conversation.
Decision. V6 early-show balanced — if the query is complete enough (species + at least two signal slots filled), surface 3–5 products on turn 0. Otherwise fall back to the question-first flow.
Consequence. Better turn-1 conversion. The risk of false-positive recommendations is handled by the user-correction loop — the guardrails catch the model when the heuristic is wrong. Compact tool results (75K→5K tokens) keep the latency down.
SMTP (aiosmtplib + atthost24) over Resend HTTP API
Context. BOK escalation emails go out from the bot when it hits its competence wall. The obvious choice is Resend — clean API, good DX, bounce tracking dashboard.
Decision. Pure SMTP via aiosmtplib through atthost24, port 465, use_tls=True. Sender = real JD branding. No third-party API key in the codebase.
Consequence. We lose Resend’s admin panel. We gain forensic logging in the same conversation_metadata that powers the dashboard, plus zero external dependencies for a load-bearing customer-facing email.
Self-hosted fonts — RODO JD-139
Context. Default Next.js + Google Fonts loads font CSS from fonts.googleapis.com, which leaks the visitor’s IP to Google before any consent dialog. LG München 2022 made this an actual GDPR fine vector, not a theoretical one.
Decision. Reuse john-dog-theme/assets/fonts/ (Oswald + Cooper Hewitt, already self-hosted on the same domain). Declare via @font-face in the widget’s scoped CSS. Map weights manually (500→400, 600→700) where the theme doesn’t ship the exact weight.
Consequence. Zero IP leaks to Google before consent. Same fonts on website and widget. Slightly larger Cooper Hewitt file because we ship the unsubsetted version — acceptable trade for the compliance certainty.
12 — Six-year timeline
Six phases from a 2020 custom theme to an AI-equipped store in 2026.
John Dog never lived in a quarterly roadmap. Each phase below earned its place by becoming load-bearing on the live store before the next one started.
Phase 1 — 2020 launch
Custom WooCommerce theme commissioned; cart, checkout, product page and category templates rewritten from the ground up. PHP + SCSS + ACF skeleton, ~40 ACF field groups in the first six months. Weekly increment cadence started immediately — no waterfall handover, no “go live and walk away”.
Phase 2 — 2021 logistics build-out
Allegro API integration with label generation + printer queue + warehouse scanner integration. DHL and InPost Paczkomaty template overrides. Per-carrier checkout preview so customers see accurate cost + ETA before they pick. Warehouse workflow automated end-to-end — from cart to printed label.
Phase 3 — 2022 catalogue scale
Catalogue grew past what native WooCommerce variants could handle. Refactor to ACF-based linked variants for multi-attribute SKUs. FiboSearch integration for faster AJAX-driven product search. Performance push: Webpack 5 upgrade, Brotli + Gzip emission, vanilla-lazyload for images and iframes. Page weight halved, build time halved.
Phase 4 — 2023–2024 editorial surface
Store grew an authority layer: Puppy Zone (dedicated CPT aggregator for new-pet content), Podcasty (seven-category submenu with SVG mask icons, 15+ commits iterating on icon sizing alone), Blog with proper taxonomy and tag filtering. Cross-sell popup in checkout with stock-availability filter so out-of-stock items quietly drop from recommendations.
Phase 5 — 2025 UX polish
Guest wishlist with cookie-based persistence that merges on login. Mobile UX overhaul (100dvh for iOS, font-size 16px+ to defeat the input-zoom trap, wishlist icon promoted to its own slot in the mobile menu). Modern CSS adoption with @supports selector(:has()) graceful degradation for older browsers. Z-index refactor to stop overlays fighting each other.
Phase 6 — 2026 AI layer
Sibling repo johndog-ai joined the platform. Five apps in a monorepo (chatbot widget + dashboard + ranking + translator + webui). Chatbot agent iterated V1→V6 over six weeks until the 4-layer guardrails caught every failure mode worth catching. Internal Open WebUI with Presidio PII masking for the team. Ranking Tool with Gemini 3.1 Pro (1 M context) for editorial articles. BOK escalation through aiosmtplib. Same VPS, separate Docker stack, separate database.
Like what you see?
Let’s build the next one.
From a blank page to a working product — AI, automation, full-stack engineering. Get in touch and let’s talk about your idea.