Online store2026-08-21

Hemax Pro Wash — cleaning products for car washes, carpet cleaning and commercial vehicles

The distributor of professional chemistry from Leskovac sells the same product in several packages, and each package has its own price, code and condition. We needed a catalog that supports this without copying products, a store where you can order without opening an account and a panel where the owner himself changes the prices. All in your own code, no platform and no monthly subscription.

Hemax Pro Wash site homepage
Homepage - three brands and a program for five types of washing
Client
Hemax Pro Wash
Activity
Professional chemistry for laundries, carpet services and trucks
Area
All of Serbia, headquarters in Leskovac
Type
Web store with catalog and admin panel
Technologies
PHP 8.3 · SQLite · vanilla JS · WebP derivatives
Site
hemaxprowash.com
Status
Live site, in active maintenance
Technology
PHP 8.3 · SQLite · Vanilla JS · WebP srcset · PWA panel
01

Problem and context

What Hemax Pro Wash does

Hemax Pro Wash is a distributor of professional washing chemicals. It supplies self-service and classic car washes, carpet services and companies that wash trucks. The headquarters and warehouse are in Leskovac, and the goods are sent by courier throughout Serbia.

There are three brands on offer — Golden Chimigal, Raysick and Cristal Finish: active foams and shampoos for non-contact washing, shampoos for machine washing carpets, products for plastics and leather, rim cleaners, fragrances, microfiber accessories and roto brushes. The same product usually comes in several packages, from a bottle for home use to a 25-kilogram canister.

The customer is not just a laundromat. Part of the range goes to hobbyists and people who wash their car once a month, so the catalog had to speak to both: the professional who knows the product code and calculates the price per kilogram, and the person who buys active foam for the first time and doesn't know the difference between pre-wash and shampoo.

On the website, the client states that he has been a seller on KupujemProdajem since 2017, with 354 positive and no negative ratings. It was also the strongest trump card, which until then stood everywhere except on its own website.

Categories and popular products on the Hemax Pro Wash site
Categories and popular products on the front
02

What needed to be solved

The chemistry store looks simple until you look at what sits behind a single displayed price:

  • One product, multiple packages. Active foam comes in 1, 10 and 20 kg. These are not three products but three sizes of the same thing, each with its own code, price and condition.
  • Same product, multiple uses. The stain remover is a carpet service, interior deep cleaning, and hobby shelf. If one category must be chosen, the catalog lies.
  • Prices change more often than the code. The client sends a new price list when his supply changes — it must not be a change in the code and a new publication of the site.
  • Ordering without registration. A car-wash operator will not open an account to order two canisters.
  • Paperwork supplied with the goods. With the order, you need a receipt with codes from the fiscal cash register, in the form the client is used to.

In addition to that, what goes with every Serbian store: conditions of sale, complaints, right of withdrawal within 14 days and privacy policy, written for this business and not copied from someone else's site.

In the first version, there was also a B2B part with a special price list and orders for companies. After a few weeks, it was removed at the request of the client: one public price list for everyone, and the agreement with the companies is made by phone. It is better to delete a system that no one uses than to maintain it forever.

Continuation of the homepage of the Hemax Pro Wash site
Homepage, further down
03

Catalog and products that appear in multiple categories

Catalog today has fifty products distributed in six programs: self-service laundries, classic laundries, carpet service, deep wash, truck wash and hobby variant. Below that are sub-levels — exterior, interior, fragrances, equipment and accessories — and within them groups such as rim cleaners or leather products.

The relationship between product and category is intentionally many-to-many. The product has one primary category and as many additional ones as needed, so the same stain remover appears in Carpet Servis, and in deep washes, and in the hobby shelf — without duplicating records. A category with children displays subcategories, a category without children displays products, and pulls them from the entire subtree below it, not just its own.

The sequence was a separate story. The owner has his own order of the catalog and a different order within the individual category — in the carpet service, machine wash shampoos go first, in contactless washing, foam first. That's why there are two orders, global and by category, and both are draggable in the panel. Sorting that the customer sees on the site (recommended, price, name, newest) starts from that manual order.

Hemax Pro Wash on the phone
Same site on the phone
04

Size is not a separate product

Package sizes are implemented as variants of one record. Each variant has its own label (1 kg, 10 kg, 20 kg), its own code, price, information on whether it is in stock and a fiscal-register code. That's why the card in the catalog says "from" and the lowest price, and on the product page the customer chooses the package and the price updates immediately.

The basket remembers the product and the selected size, so two packages of the same item remain separate cart lines instead of being merged.

The most useful refinement came later, from day-to-day use. The client wanted to turn off only one size when it ran out, and keep the others on sale — deactivating the entire product would also hide the 5 kg package that remains in stock. Now in the panel that size switches to "out of stock": the site shows it crossed out and prevents selection, the first available becomes the default, the “from” price uses only available package sizes, and if all sizes are turned off the product is reported as currently unavailable. The cart rejects the unavailable variant even when someone tries to add it in a roundabout way.

05

Cart, order and invoice

Order without registration. The customer fills in the information, chooses payment by cash on delivery or payment to the account and sends the order — in one step, without a required account and without online card processing.

The price is calculated on the server. What arrives from the browser is not viewed: the amount, shipping and threshold for free shipping are re-derived from the database before the order is entered. There is a strip in the basket that shows how far away it is from free shipping, because that's the question the customer asks anyway.

When the order goes through, the customer receives a confirmation email, and the owner receives a notification with all items. An A4 invoice is printed from the panel for each order: logo, issuer data, current account, items with cash register codes, total amount and amount written in letters. The account number is assigned at the first opening and does not change afterwards.

The cash register codes were a task in themselves. The client sent them as a photo of a list with about sixty items; they are entered both by product and by package, they are frozen in the order at the time of purchase and appear on the account. Three codes from that list didn't have their own product on the site — that was reported to the client instead of guessing what they belonged to.

06

Technical performance

The site is PHP 8.3 and SQLite, without WordPress and without plugins that require updates. The front is plain JavaScript, without frameworks.

Addresses are clean — /catalog, /category/carpet-service, /produkt/blue-beast. One detail is worth noting: the usual way to achieve this, try_files $uri $uri.php, serves the source PHP code as a plain file as soon as the requested file exists. For a time, the store was able to issue its own code instead of the page. Resolved by a named fallback that does a rewrite on .php and returns the request to processing, so the source is never served statically.

The database is intentionally out of the repository. Products, prices and orders live on the server, and the code is published separately. Catalog migration is write-or-update per item, never delete, while preserving what exists only in the live database — the availability of individual packages and checkout codes. Without that save, one catalog refresh would wipe out a week of work in the panel.

One failure only showed up under parallel load. The product view counter was writing to the database without waiting for a lock, so on simultaneous visits the product page could return a 500 error. A wait for a lock was added, and the counter was isolated so that its failure did not crash the page: twelve parallel requests before the fix gave five errors, after none.

07

Images: 39MB photos in 260px cards

The client sent the product photos by email, in full resolution, with the express request that they not be processed. The request is reasonable — these are his photos and he knows how he wants his merchandise to look. The problem was that the catalog was dragging about 39MB of images to display in 260px wide tabs.

The solution does not touch the originals. Next to each photo are smaller WebP versions of 320, 640 and 960 pixels, and the page offers them through <picture> and srcset, so the browser itself chooses according to the screen width and pixel density. The original remains as a reserve for everything that does not have its own derivative. Practical: the 829 KB photo is loaded in the card as a 9 KB file, and the cover background image is reduced from 1.2 MB to 62 KB.

The magnifying glass is retained on the product page: mouseover enlarges part of the image, and clicking opens the full version with zooming and dragging. There's a separate, larger copy of the image for that — the only one that isn't intentionally shrunk.

08

Speed, measured not estimated

Once the images are sorted, there's the rest of what usually slows down the first rendering: fonts and styles.

Fonts are downloaded from Google's servers and hosted on the same domain, in a version that only carries Latin with our letters — one of them dropped from 352 KB to 208 KB. The styles are inserted directly into the page header, so the first view does not wait for any additional files. The cover background image has a special, smaller version for phones and is announced to the browser in advance.

Measured through PageSpeed Insights in June 2026: desktop 100/100/100/100 — performance, accessibility, best practices, SEO — and mobile 90/100/100/100. That 90 on mobile is a realistic ceiling for a site with a big photo above the fold, because the mobile test runs with a quadruple slower processor and a slow network. On a real phone over a good connection, it's not the same picture.

There's also an easily overlooked rule: above-the-fold content must not wait for scrolling animations. The title and buttons at the top are excluded from that system, because otherwise the largest element of the page would wait for JavaScript to execute.

09

Moving to own domain, SEO and visibility

The store first lived on my subdomain, until the client registered his domain. The move to hemaxprowash.com was done as one jump: the old URLs were permanently redirected to the new ones with a preserved path, the canonical URLs and sitemap were transferred to the new domain, and the domain was added and confirmed in Google Search Console via DNS records. Google's change address option was intentionally not used, as it would have moved our entire domain from where the subdomain was — a permanent redirect and a new item in Search Console did the job without that damage.

The content section is four pages per type of work, outside the catalog: chemicals for laundries, for self-service laundries, for carpet services and for truck washing. Each has its own text, problems it solves, a selection of products from the database and its own questions and answers. These are pages that capture searches by business, while categories capture searches by product.

Structured data is connected in one graph: company with address, working hours and coordinates, site with search, shop, questions and answers, breadcrumb data on every page and product information with delivery time and right of return. With products with multiple packages, the price range goes, and with those without a price, the price is simply omitted instead of invented. The sitemap contains about eighty URLs, and robots.txt intentionally lets AI search crawlers in.

10

Panel: everything that arrives and everything that changes

The owner works from his own panel that stands on a separate subdomain and can be installed as an app on a phonelication.

In it are products with variants, codes, categories and a drag-and-drop order; orders with all the customer's data written immediately in the list, where the phone number and email are clickable; messages from the contact form; accounts; settings such as the threshold for free shipping; and traffic overview.

Visit statistics are solved without Google Analytics and without cookies: the path, source and irreversible impression of the visitor are recorded, which changes every day, so that the same person counts once during the day, and after that he cannot be connected to anything. There are no tracking consent banners because there is nothing to ask for — the site only uses technical cookies for the basket.

Every evening, a short report arrives by email: how many orders there were, how much traffic, how many inquiries, how many visits and what is waiting for processing. It's the only report the owner actually reads, because it gets where he's looking anyway.

11

Outcome

What failed and what the client received

Two things are worth noting because they were real bugs, not cosmetics.

First: for a while the orders didn't go through at all. Entry to the database was being rejected due to a connection to a table that was deleted along with the B2B system — the database was rejecting every order entry, silently, with an error at the very end of the process. No order could go through until the table was restored. The first real order arrived the same day after the repair.

Second: the customer typed gamil.com instead of gmail.com in his address, the order confirmation was rejected, and the rejected email fell into the owner's inbox as "undeliverable". Since then, the sender has been separated — automatic messages go from an address that does not receive replies and rejected emails are collected there, while the customer's reply still arrives at the working address. The owner's inbox remained clean.

What he has today: a store the client owns, with codebase and server database that are copied and moved. No monthly platform subscription, no commission per sale, no plugins that break on the next update. He changes the prices and packages himself, the order arrives both by email and in the panel, the invoice is printed from the same place, and customers find it via the contact page and the phone that is on each page.

FAQ

Frequently asked questions

Why custom PHP and not WooCommerce or Shopify?

Because of the way this catalog has to work. Packages as variants with their own checkout code and balance, product standing in multiple categories, and two separate orders — global and by category — in the finished platform would be plug-in over plug-in. Plus, there are no monthly subscriptions or sales commissions.

Can the owner change prices, package sizes and their order?

Yes, it all goes through the panel: prices, packages, codes, categories, images and order that is dragged with the mouse. The database is the source of truth and is on the server, so the change is valid immediately, without publishing a new version of the site.

How to display a product that has multiple packages?

As one product with a choice of size. In the catalog, you can see the lowest price marked "from", and on the product page, the package selection changes the price and code. In the basket, each package is a separate row.

What happens when one size disappears from stock?

Only that size is canceled, not the entire product. The site shows crossed out and cannot be selected, the default becomes the first available, and the "from" price only counts what can actually be bought. If all sizes are turned off, the product is reported as currently unavailable.

Does the customer have to open an account to order?

No. The order is made in one step, without registration and without a card on the site — payment is by cash on delivery or payment to the account. The server calculates the total amount and delivery from its database, not the browser.

Does this store have monthly costs?

There are no platform subscriptions or sales commissions. The domain and server are paid for, and the maintenance is negotiable — the code and database are the client's and can be moved to another contractor if ever needed.

Next step

Have a project that needs to work better?

Send me your goal and current situation. I will reply with a concrete scope proposal.

Start a project