odnaco.me: a flight-gear shop — from RiotJS to web components

Client
Odnaco
Year
2026
What we did
ProcessWire websites
Stack
ProcessWire, PHP, Static cache, Native web components, Vanilla JS
Website
odnaco.me

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.

6
web components in place of Riot tags
The configurator, the add-to-cart button, the cart, the header mini-cart, the loading indicator and the icons: 16 KB of source for all of it
B
18.7KB
of JavaScript for the whole site
One file, compressed, the shared forms and images library included. There is no framework in it
B
3.8KB
of HTML for a product page
Compressed. Apache serves the page as a ready-made file; an ordinary visit never starts PHP
B
2017
first order through the site
The catalogue dates from December 2015, the shop has taken orders since July 2017
B
odnaco.me

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

Four categories and 21 products. A card shows a “from” price: the final one depends on the chosen specification. On a phone the category menu waits behind a button in the header.Four categories and 21 products. A card shows a “from” price: the final one depends on the chosen specification. On a phone the category menu waits behind a button in the header.
Four categories and 21 products. A card shows a “from” price: the final one depends on the chosen specification. On a phone the category menu waits behind a button in the header.

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 cart and the order form share one page. The order, with every chosen option, becomes a page in the admin, the manager and the customer get their emails, and reloading the page does not send them twice.The cart and the order form share one page. The order, with every chosen option, becomes a page in the admin, the manager and the customer get their emails, and reloading the page does not send them twice.
The cart and the order form share one page. The order, with every chosen option, becomes a page in the admin, the manager and the customer get their emails, and reloading the page does not send them twice.

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.