E-commerce

Hand-built online stores on a pure stack — no CMS, no frameworks, no heavy infrastructure. The full cycle: catalog, cart, online payment, admin panel, SEO and analytics.

If you are not a developer

An online store is more than a shelf of products. It is stock numbers that have to match the real warehouse, a payment that has to reach your account, and an admin panel where you change prices yourself without calling anyone. All of that here is written by hand rather than assembled from ready-made blocks.

What that means in practice: the store does not depend on someone else’s service and its monthly plan, it runs fast on cheap hosting, and it does not break when a third-party plugin updates. The honest downside — changes are made by a developer, not by you in a builder’s dashboard.

E-commerce

Vector Pola — flooring store

RoleSole developer — full-stack: data architecture, backend, frontend, admin panel, SEO and deployment
StackPHP 8 · SQLite (PDO) · HTML · CSS · JavaScript — no CMS, frameworks, npm or Composer

A live flooring store — laminate, SPC and rigid vinyl, engineered and parquet board — for a retailer with two showrooms, in Moscow and Krasnogorsk. The catalog holds around 7,600 active products: 3,300 laminate items, 3,900 in SPC and rigid vinyl, plus engineered and parquet board. The storefront is written in plain PHP with no CMS and no frameworks, runs on cheap shared hosting and needs no build step at all. The key engineering decision is a two-layer store: a canonical 17.5 MB JSON file and a derived SQLite cache that the storefront actually reads from. Orders arrive as requests in Telegram rather than online payments — that is how the client's sales process works.

What was neededThe client runs two showrooms and a catalogue of 7,600 items. It had to go online without collapsing under its own weight or demanding an expensive server.

What came out of itThe store runs on ordinary cheap hosting, products update from a data file rather than by hand, and there is no monthly fee for an engine.

Data architecture

The core idea: products.json is the source of truth, catalog.sqlite is a derived read cache. The storefront no longer decodes 17.5 MB of JSON on every request — it queries a ready-made database over PDO.

  • The admin panel writes to JSON, and the database is rebuilt once per request via register_shutdown_function — essential for the importer, which saves products in a loop and would otherwise trigger a rebuild on every row.
  • The rebuild is atomic: it writes catalog.sqlite.tmp and swaps it in with rename. On failure the temporary file is deleted and the old database stays alive — live requests never hit a half-written base.
  • Two tables: products (22 columns, active/in_stock/popular/promo flags and a search_text column = lower(name + brand) for case-insensitive search in Cyrillic) and product_facets, holding the filter values expanded per product. Five indexes on products, three on facets.
  • Resilience to schema drift: queries against the newer columns are wrapped in try/catch. If the PHP is deployed before the database is rebuilt, neither the storefront nor the admin panel returns a 500 — they degrade gracefully to an empty block or a JSON fallback.

Catalog and filters

Around 7,600 active products across 4 of the 9 categories; the rest show a “section being filled” state instead of an empty page.

  • Faceted filters configured against the real specifications of each category: brand, abrasion class, thickness, installation type, wood species and finish. The logic is OR inside a facet and AND between facets, with a product count next to every value.
  • A price range that hints at the real bounds for the category, an “in stock only” filter, price sorting and server-side pagination at 24 products per page.
  • Filters auto-submit: instantly on click on desktop, on the “Show” button in the mobile drawer, and on Enter for the price fields.
  • Search across name and brand with multi-word queries: every word has to be found in the normalised search_text.
  • The catalog data is normalised: no zero prices, no duplicate slugs, brands filled in and specification keys canonicalised.

Product page and ordering

Flooring comes with its own quirk: it is sold by the pack, while the buyer thinks in square metres.

  • An area calculator: enter the square metres and it works out the number of packs (rounded up), the actual area covered and the final total. The first calculation is rendered on the server, so the page still makes sense with JavaScript off.
  • A gallery with photo switching, specifications in a table, “Add to cart” and “One-click purchase” buttons, WhatsApp and Telegram share links and copy-to-clipboard.
  • A client-side cart (localStorage) that recalculates packs for every line; checkout submits a request rather than a payment — the order contents and total go to Telegram as text.
  • A two-layer image placeholder: an empty images array falls back on the server, a broken link falls back through onerror on the client.

Requests and notifications

  • Every form — consultation, one-click purchase, cart, delivery, designer partnership — goes through a single handler into a Telegram bot, tagged with the source of the request.
  • Separate timeouts for connecting and for the whole operation (5 s and 15 s). If the API fails, the request is appended to a closed data/leads-failed.log — the lead survives even when Telegram is unavailable.
  • The promo subscription is written to JSON under flock, deduplicated by email: submitting again updates the name, phone and date instead of adding another row.
  • On the client, a single form initialiser reads the endpoint and success message from data attributes; the phone mask and validation are reused by every form.

Custom admin panel

Entirely hand-written, behind a login on PHP sessions.

  • Catalog: product CRUD, server-side search and pagination, and “List” / “Popular” / “On sale” tabs — checkboxes on the product drive the carousels on the homepage, and an empty section simply is not rendered.
  • XLSX import and export through a hand-written reader and writer built on ZipArchive and manual XML — no Composer, no external libraries. The export expands every specification key into its own column, the template ships with an example row, and fixed versus additional columns are colour-coded.
  • The importer upserts: first by slug, then by SKU; it repairs SKUs like “1.0” that Excel and Google Sheets produce from whole numbers, and it can pull photos in by URL.
  • Image uploads: a centre crop to an 800×800 square and conversion to WebP on the server (GD), accepting both files and URLs, with a MIME check.
  • Subscriptions: a list with the newest first, deletion over POST with a confirmation and a PRG redirect, and export to CSV (with a BOM for Excel) and XLSX.
  • Login protection: an arithmetic captcha, a 5-minute lockout after 5 failed attempts, and password recovery over Telegram and email. A manual database rebuild sits on its own page.

SEO

  • A dynamic sitemap.xml straight from SQLite: static pages, only the non-empty categories and every active product with lastmod from its update date — around 7,600 URLs today.
  • Schema.org / JSON-LD: Product + Offer with priceValidUntil, itemCondition and dynamic availability on product pages, and BreadcrumbList on products, categories and the catalog index.
  • Deliberate index control: clean pagination is indexable with a self-canonical and rel prev/next, while filter and sort combinations go to noindex,follow so they do not breed duplicates.
  • SEO titles and descriptions are set per product in the admin panel, with a fallback template built from the name. Human-readable URLs /catalog/{category}/{slug}/ via mod_rewrite, with Cyrillic transliterated into slugs and uniqueness guaranteed.
  • Internal search and the cart are closed to indexing, and robots.txt and the sitemap agree with each other.

Performance and hosting

Speed comes from the storage architecture rather than the infrastructure: no CDN, no cache layer, no background daemons — cheap Beget shared hosting and deployment over rsync.

  • Dropping the 17.5 MB JSON parse on every request in favour of indexed SQL queries is the main win in response time.
  • All ~7,800 product photos are normalised to WebP 800×800 and the brand logos to WebP 320×180. The preparation pipeline is local Python/PIL scripts that trim white margins and cap upscaling.
  • Preload and fetchpriority on the hero image (LCP), lazy loading for the rest and explicit width/height — zero layout shift.
  • Gzip and static caching headers in .htaccess, with asset cache-busting through ?v=N.
  • Analytics: Yandex.Metrica with webvisor and clickmap.

Security and robustness

  • The /data/ folder with the product JSON and the subscriber list is closed off both in .htaccess (a RewriteRule with [F]) and in robots.txt; config.php with the bot token is denied direct access.
  • PHP execution is disabled in /uploads/ and only images are served; the admin panel's internal files are blocked through FilesMatch, and each one additionally checks that it was not called directly.
  • Meaningful 404s for missing products and categories: the check covers not only the slug but also that the category in the URL matches the product's own category.

What makes it interesting

  • A 7,600-product catalog running on shared hosting with no CMS, no MySQL, no npm and no Composer — thanks to a considered storage design rather than raw capacity.
  • JSON as the source of truth with SQLite as a derived read cache: rare writes, frequent reads — a trade-off that fit this project better than a full DBMS.
  • The engineering details that only show up in production: atomic database rebuilds, one rebuild per request instead of thousands during an import, graceful degradation on schema drift, and a fallback log for lost requests.
  • A hand-written XLSX reader and writer instead of a heavy library — the client maintains the product range in Excel, and it works with zero external dependencies.
  • The full cycle by one person: data architecture → backend → frontend → admin panel → image pipeline → SEO → deployment.
E-commerce

Wergrauf — plumbing store

RoleSole developer — full-stack: architecture, backend, frontend, integrations, SEO and deployment
StackPHP · HTML · CSS · JavaScript — no frameworks, CMS or database

The client rebranded and wanted to raise the level of the site. For the new brand I built a plumbing store from scratch on a pure stack — no CMS, no frameworks, no MySQL. Product data lives in JSON files that sync automatically with Google Sheets. The full cycle: a catalog of 9 categories, product pages, cart, checkout and online payment, moderated reviews, a custom admin panel, e-commerce analytics and SEO. The site holds 90–100 on PageSpeed Insights on both desktop and mobile.

What was neededThe client rebranded and wanted a step up from the old site — with payments on the site and the ability to manage products without calling a developer.

What came out of itProducts are kept in familiar Google Sheets and sync to the site on their own. Payments, moderated reviews and an admin panel are in place, with no platform subscription.

Architecture

A deliberately light “database-free” architecture: instead of a CMS and SQL, file storage plus a Google Sheets integration acting as a convenient content panel.

  • The data source is the Google Sheets API: one spreadsheet, a separate sheet per category. A PHP sync script pulls the data through the API and saves it as JSON — the content manager maintains the product range in a familiar spreadsheet and the site updates at the press of a button.
  • Image localisation: during sync, external product photos are downloaded to the server, converted to WebP and renamed by slug. Idempotent, with a fallback if something fails.
  • An override layer: manual edits from the admin panel (meta tags, hidden products, ordering) are applied on top of the spreadsheet data — the spreadsheet stays the source of truth, but any field can be overridden locally.
  • Server-side rendering of the catalog for proper internal linking and SEO; client-side JS only handles filtering and sorting.

Key features

Catalog and products: 9 categories, a single server-side template for both the product page and the catalog; product pages with a gallery, photo switching, specifications and “similar products” / “from this collection” blocks; client-side filtering by price (dual slider), model, colour and collection, plus sorting.

Cart, order, payment: a client-side cart (localStorage), checkout and one-click purchase, online payment via Ozon SBP, an order status page that polls the payment, and new-order notifications in Telegram through a bot.

Reviews: a form with up to 4 photo uploads and three layers of anti-spam protection (honeypot + fill-time check + arithmetic captcha), stored as JSON and moderated through the admin panel.

A new-arrivals subscription that stores the subscriber list and shows it in the admin panel.

Custom admin panel

Entirely hand-written, with authentication on PHP sessions.

  • A dashboard with per-category statistics.
  • One-click manual sync with Google Sheets, with a result log.
  • Viewing and editing the products in each category, including meta tags (title/description) for SEO.
  • Adding manual products on top of the synced ones and editing the homepage content.
  • Review moderation, a subscriber list and a sync log.
  • Service tools: batch image optimisation and system junk cleanup.

Performance (PageSpeed Insights)

The site was brought up to 90–100 Performance on desktop and mobile.

  • Responsive image sizes: for every product photo the sync generates derived sizes (product page ~600px, thumbnail ~160px) alongside the original used in the gallery — product images are 65–70% lighter, and the thumbnails under the main photo went from ~65 KB to ~2 KB.
  • WebP everywhere — products and logos (via <picture> with a fallback).
  • Cumulative Layout Shift eliminated: explicit width/height on every image.
  • Load prioritisation: fetchpriority on the main image (LCP), lazy-loading for the rest, preconnect to the analytics domain, defer on scripts.
  • Contrast, headings and fonts tuned for mobile.

SEO

  • Schema.org / JSON-LD: Product + Offer (dynamic availability and priceValidUntil), BreadcrumbList on product pages, Organization + WebSite on the homepage.
  • Canonical URLs, keyword-first title tags, a correct heading hierarchy, <main> landmarks and semantic markup.
  • Auto-generated sitemap.xml and robots.txt.
  • A YML feed for Yandex.Direct (product campaigns, smart banners) that refreshes itself after each catalog sync.
  • Accessibility: ARIA labels, WCAG contrast, accessible navigation.

Analytics

Full e-commerce analytics on Yandex.Metrica: goals and a dataLayer on the key actions (add to cart, order, one-click purchase, successful payment, subscription), protection against double-firing goals, webvisor and clickmap. Purchases are tracked with de-duplication during payment polling.

What makes it interesting

  • Zero dependence on heavy infrastructure: no CMS, no SQL, no framework — and still a full store with payments, analytics and an admin panel.
  • Google Sheets as the content backend — an unconventional integration, but a convenient one for the client.
  • Performance as a deliberate priority: image optimisation is built into the data pipeline rather than bolted on afterwards.
  • The full cycle by one person: architecture, backend, frontend, integrations, SEO, deployment and support.
E-commerce

Hizberg — plumbing store

RoleSole developer — markup from the client's mockup, backend, payments, deployment
StackPHP · HTML · CSS · JavaScript (jQuery) — no frameworks, CMS or database

An early commercial project: a plumbing store built from scratch from the client's mockup. A deliberately simple “database-free” solution — content is hardcoded into the pages, products and prices live in PHP files, and orders are stored in a file structure with no DBMS. Even so, the store covers the full sales cycle: catalog, product pages with colour and quantity selection, cart, checkout, online payment, order status tracking, marketplace review import and customer notifications. A pragmatic MVP that was enough for the business to make real sales at the time.

What was neededThe brief was a plumbing store from a ready design — with online payment and order tracking, and as little to maintain as possible.

What came out of itThe whole purchase cycle runs without a database at all: nothing to break, the cheapest hosting will do. An early project, and the code shows it — it stays here honestly.

Architecture

A static file structure with no DBMS and no template engine: each product is its own folder with an index.php and images, prices are pulled out into variables in a single shared PHP file, categories (mixer taps, shower systems, spare parts, accessories) form a catalog of nested folders, and shared blocks (header, menu, footer) are pulled in as includes. A deliberately blunt approach, but transparent and dependable for a static product range.

Catalog and products

Product page: gallery with zoom, colour selection by switching between variants, a quantity counter that recalculates the price on the fly, and breadcrumbs with Schema.org markup. The cart is client-side (localStorage), with discount coupons and free delivery above a threshold amount.

Orders and payment

Orders are kept in a file structure: each one gets a folder with its status, the customer's details and a generated order page. Online payment by card and via SBP through Tinkoff acquiring (payment session over the API, redirect to checkout, automatic status change on successful payment); alternatively by bank details (QR + PDF) or cash. The customer receives a tracking code and a status tracking page.

Admin panel

Kept as simple as possible: a single log of all requests where every row carries action links — “paid / fulfilled / closed”. The manager handles orders straight from the log: confirms payment, changes the status, closes it. Exactly as much functionality as the order flow needs, with no DBMS and no heavy interface.

Reviews

Review import from marketplaces (Ozon, Wildberries): a parser reads saved pages, picks out highly rated reviews, cleans and deduplicates them, and puts them into a single list that loads more as you scroll. The links point back to the original marketplace listings.

Notifications

When an order is placed, the customer receives an email and an SMS with the order number and tracking code.

SEO

Unique title/description on every page, human-readable URLs following the catalog structure, breadcrumbs with structured data, sitemap.xml, robots.txt and a favicon — a basic but deliberate technical minimum.

What makes it interesting

An early “from scratch, from a mockup, no CMS or frameworks” build, put together by hand in about a week (back before AI tooling) — and still a working store with real online payments and order processing. The same client later rebranded to Wergrauf on a mature data-driven architecture, so the two projects together show how the approach grew.

What next

If your case looks similar, see how much a website costs — it breaks down what makes up the price of a store and what changes it. If you need a multi-page company site rather than a store, that is the next section.