odnaco.me: a flight-gear shop — from RiotJS to web components
A suit is put together from size, colour, variant and options right on the page, the server works out the price, and the whole site’s JavaScript is 18.7 KB compressed.

A shop without a platform
Odnaco makes gear for paraglider pilots: flight suits, concertina bags for packing a wing, “doughnuts” — quick-stuff bags — and small accessories. The catalogue went up on the site in 2015; orders have come through it since 2017.
An off-the-shelf shop platform would have been dead weight here. There are a couple of dozen products, a good share of them sewn to order, and an order is not “SKU × quantity” but a suit of a given size, colour and specification. So all of it is written on ProcessWire from scratch: products and their options are ordinary pages and fields in the admin, the cart lives in the server session, a placed order becomes a page in the orders section, and emails go to the manager and to the customer.
Payment is by cash, card transfer or invoice, with no online card payments: for a shop of this size that is enough.
The catalogue


The orderable item is configured on the page
The clip above is the “Polusamosbros” suit. It comes in six sizes, made-to-measure among them, nine colours, three builds — summer, spring/autumn and winter — and eight extra options, from belt loops to a removable hood. Every choice re-prices the button at once: five clicks turn 19,000 ₽ into 21,700 ₽.
In the admin all of it is repeater fields on the product page itself: a colour has a name and a shade, a build and an option have a name and a surcharge. The manager adds an option or unpublishes it, and the configurator learns about it on the next page load, with no code change.
The server works out the price. The browser sends the cart nothing but the choice — the ids of the product, the colour, the build and the options — and the cost is summed from the prices stored on those pages. The figure on the button is a hint for the customer, not a number the shop believes.
One tag in the template
<!-- product.php: the whole configurator is one tag -->
<product-configurator uid="<?=$page->uid?>"></product-configurator>
// components never call each other: they talk over one small bus
AppBus.trigger('cart_add', uid, config) // the button, on click
// AppStore is the only code that talks to the server,
// and what it sends is a choice of ids, never a price
post('/api/cart/add', { uid, config })
// config: {"size":"M","color":"…","variant":"…","options":["…","…"]} The components do not know about each other: the button announces an event, and one shared store sends the request.
From RiotJS to web components
The first frontend was written in RiotJS 3: component tags in .tag files built by riot-compiler, RiotControl to connect them and jQuery for everything else. A sensible choice for 2017. By 2026 Riot 3 was de-facto dead and sat badly with modern build tools.
We rewrote the frontend as native web components — something the browser does by itself, with no library. Six elements replaced the tags: <product-configurator>, <cart-add>, <cart-view>, <mini-cart>, <ajax-indicator> and <svg-sprite>. Shadow DOM was not needed: the components render ordinary markup and use the site’s shared styles, as before. A small event bus took RiotControl’s place, and a copy of the cash library cut down to 27 methods took jQuery’s — exactly what the site actually calls.
The last step, in August 2026, swapped the old gulp build for ours: esbuild, styles, an icon sprite and hashed file names, so the browser keeps them for a year and never gets a stale one. The whole site’s JavaScript is now one file of 18.7 KB compressed, with no framework in it.
Cart and checkout


The catalogue is served as ready-made files
Catalogue pages rarely change and are opened often, so it is Apache, not PHP, that serves them — as ready-made HTML files from the StaticPages cache. A product page is 3.8 KB compressed and arrives without a session cookie.
A shop still needs protection against forged requests, and a file shared by everyone cannot hold a visitor’s personal token. So StaticPages cuts the token out before it saves a page, and the script fetches a fresh one with its first API request, together with the product and cart data. Every request that changes the cart carries that token; without it the server refuses.
Images come in whatever format the browser understands: an 800-pixel photo is 17 KB as AVIF, 19 KB as WebP and 37 KB as JPEG — against 286 KB for the original.


