Business sites

Corporate and business websites on a pure stack — no CMS, no frameworks. Multi-page and multilingual structure, request forms with spam protection, SEO and performance optimisation.

If you are not a developer

A company site differs from a landing page not in size but in lifespan. Services, projects, documents, branch contacts — people come back to a site like this, read it in pieces and search it for something specific. That is why it has a different structure and different demands on speed and search.

These sites are built without a CMS. That means nothing to update for security, nothing to break when a third-party component changes, and no monthly fee for the engine. Requests land in a messenger instead of piling up in a dashboard nobody opens.

Business site

MosKomDez — pest control service website

RoleSole developer — full-stack: architecture, backend, frontend, markup, admin panel, amoCRM integration, image preparation, SEO, optimisation and deployment
StackPHP 8 · HTML5 · CSS3 · JavaScript · JSON instead of a database — no CMS, frameworks, npm or Composer

A live website for MosKomDez, a disinfection, pest and rodent control company: a homepage, 27 service pages, contacts and a privacy policy, plus a custom admin panel. A complete replacement for a Tilda landing page — content migrated, the donor's technical flaws cleaned up and the SEO preserved. The key feature: a single physical site serves 80 city versions on wildcard subdomains — Moscow on the main domain and 79 towns of the Moscow region, without a single copy of the files. Requests go into the client's amoCRM and into Telegram at the same time. Everything is written in plain PHP with no CMS and no database, runs on ordinary shared hosting and needs no build step on the server.

What was neededThe site sat on Tilda, kept running into its limits and could not have separate pages for the towns around the capital.

What came out of itOne site now serves 80 city versions, requests go to amoCRM and Telegram, and search positions survived the move.

Multi-city architecture

The client wanted city landing pages covering the whole Moscow region. Copying the site 80 times was not an option: the content is edited through the admin panel and the copies would have drifted apart immediately.

  • The city registry is a single JSON file: the slug (which doubles as the subdomain), the name and four grammatical case forms, plus the “main”, “active” and “indexable” flags. The same file feeds the city list on the page and the version switcher.
  • City names are injected through placeholders — {CITY}, {CITY_IN}, {CITY_TO} and others — replaced in the finished HTML by an output-buffer callback. That makes them work everywhere at once: in templates, in copy coming from the admin panel, in alt and title attributes, in hidden form fields and in structured data. There is physically nowhere to forget to wire the substitution up in a new block.
  • Personalised: title, description, keywords, H1–H3 headings, the main and SEO copy, the FAQ, image captions and forms. Deliberately not personalised: the office address and the real locations in the work examples.
  • Host parsing is resolved before any output: an unknown subdomain returns an honest 404, and the subdomain of the main city 301s to the main domain so no duplicates are bred.
  • Canonical and og:url are computed from the current host, so they stay correct on a subdomain by themselves. robots.txt is generated by PHP: the Sitemap line points at the current host, and on city versions it is absent altogether.
  • All the subdomain logic is locked inside a single file — the rest of the code knows nothing about it.
  • The admin panel returns 404 on subdomains: 79 extra login entry points are not needed.

Content and a custom admin panel

There is a lot of content — 27 service pages, nine price tables, 80 cities — and all of it lives in JSON and is edited through a hand-written panel.

  • A tabbed editor for a service page: SEO, hero (heading, checklist, image, price), copy, a price block made of several tables, FAQ and the set of page blocks — the gallery, methods, certificates, quiz and the rest can be switched off per service.
  • Separate screens: the homepage hero, the homepage price block, the services slider, before/after work examples, cities, settings and a recycle bin that restores deleted services.
  • Every write follows the same path: validation → a backup of the previous version (10 copies per file) → a control re-parse of what is about to be written → a temporary file → an atomic rename. An interrupted request cannot leave broken JSON behind, and an error comes back as human-readable text rather than a 500.
  • Texts are cleaned through a tag whitelist with all attributes stripped: a copy-paste from Word cannot break the layout or smuggle in a script.
  • Photo upload: the file is accepted with a type check, then cropped and converted to WebP on the server through GD.
  • The city table deliberately has no “Main” column: the server takes that flag from the file in use and ignores whatever was submitted, and the row of the main city can be neither deleted nor disabled — the client must not be able to reassign the main domain by accident.

Requests, quiz and the amoCRM integration

There are six request entry points on the site: the promo bar, the hero form, “assign a specialist”, a modal window, two forms on the contacts page and the quiz calculator.

  • A four-step quiz plus a final screen. On a service page the matching option of the “problem” step is pre-selected — the visitor has just read the heading, so there is no point making them click the obvious. The client sends only answer indexes; the server takes the texts from its own JSON, so an arbitrary string cannot be submitted.
  • The city of a request is derived from the host, not from a form field — it cannot be forged.
  • Endpoint protection: a honeypot, server-side validation of the phone number and the consent checkbox, per-character field trimming, and a limit of 5 requests per IP in 10 minutes kept in a file under flock.
  • Integration with the client's live amoCRM, with no staging account: a request lands in the “Unsorted” inbox of the right pipeline — individuals and companies are split by the first quiz step. Quiz answers are mapped onto real CRM fields, ad tags onto service API fields, and everything else goes in as a note.
  • Duplicate lookup by phone is mandatory rather than optional: the telephony system and a callback service already write into the same CRM. amo's search is a plain string search across all fields at once, so the lookup uses the last ten digits of the number and then re-checks the contact it found — otherwise a request sticks to somebody else's card.
  • Not a single field ID is written by hand: the mapping is generated by a script from an account dump, verified by a separate command, and the field layout of a request is dry-run locally without touching the network.
  • Telegram was kept as a second channel at the client's request: a request counts as accepted if at least one channel worked, and if none did, it is appended to a private log on disk. A lead must not be lost — nobody writes in a second time.
  • A reverse webhook: when a deal moves to its final stage, a message goes into a Telegram group with the city, the problem, the contacts and a link to the deal.
  • First-touch ad tags (utm, gclid, yclid, referrer) are stored in a cookie on the base domain — it survives the geo-redirect to a subdomain, so the source is not overwritten and paid traffic does not turn into direct visits.

Detecting the visitor's city

A separate requirement from the client: a visitor should land on their own city's version straight away.

  • The browser is asked for geolocation; on refusal or timeout the city is resolved by IP — both branches go through DaData.
  • The name is normalised (the city from the service and the city from the registry are compared without the “g.” prefix, without case and without the letter “ё”) and looked up in the registry; a city that is not listed means staying on Moscow.
  • A listed city means moving to its subdomain along the same path, followed by a pop-up: “Need treatment in …?” — “Yes” stays, “No” opens a full-screen city list with live search.
  • The result and the confirmation are stored in cookies on the base domain so subdomains can see each other's — detection runs once per visitor.
  • The service's free tier allows 10,000 requests a day, so the endpoint keeps its own limit — 20 lookups per IP per hour, with the counter in a file under flock.

SEO

  • Unique metadata for each of the 27 services, personalised per city.
  • JSON-LD: LocalBusiness with phone numbers, an address broken into parts and the service area of the current city, plus FAQPage — but only where the questions block is actually rendered on the page.
  • sitemap.xml is assembled from the service index; the modification date is taken from the content file and updates itself whenever something is edited in the admin panel.
  • Flat human-readable URLs, with the canonical address enforced by a 301 that preserves UTM tags.
  • City versions are closed to indexing by a meta tag and an HTTP header, while robots.txt does not disallow them — the crawler has to come in and see the noindex. Letting a city into search is a switch in the admin panel.
  • Open Graph and Twitter cards on every page, and a meaningful 404 page with noindex.

Performance and accessibility

PageSpeed Insights, production domain, August 2026. Service page: Performance 100, Accessibility 100, Best Practices 96, SEO 100 on mobile, and 99/100/96/100 on desktop. Homepage: 92/100/88/100 on mobile.

  • The speed comes from markup and assets, not from infrastructure: no CDN, no cache layer, no bundlers on the server — ordinary shared hosting.
  • Self-hosted Roboto instead of Google Fonts removed about 2 seconds of render blocking and a chain of five external requests: three variable woff2 files by subset, with Cyrillic and Latin preloaded.
  • In-house minification and asset versioning by file modification time: CSS 91.6 → 54.6 KB (10.2 KB over the wire), JS 33.1 → 14.3 KB (4.7 KB). If a minified file is stale, the source is served instead — the site does not break.
  • All images are WebP with explicit dimensions, srcset and fetchpriority on the LCP image, lazy loading for the rest and content-visibility on the lower sections. CLS is close to zero.
  • Accessibility 100: AA contrast everywhere coloured text sat on a light background, aria labels in forms, touch targets no smaller than 24 px, modals closing on Esc and a prefers-reduced-motion branch for the animations.
  • The four points lost in Best Practices are the price of requesting geolocation on page load: Lighthouse wants that permission behind a click, but then the automatic jump to the right city becomes impossible. A deliberate trade of points for functionality, agreed with the client. Before geolocation was added the site scored 100 in all four categories.

Security and reliability

  • Secrets live only in a config outside the repository; not a single token reaches the HTML or the JS.
  • The folders with content, service PHP, template fragments and developer scripts are closed to direct access, with a fallback in case mod_rewrite is switched off.
  • Output is escaped everywhere data enters the markup; structured data is encoded so that a stray </script> in the text cannot tear the page apart; directory traversal is cut off on every path assembled from input.
  • The admin panel: a password hash, timing-safe comparison, a delay after a failed attempt, session regeneration, a CSRF token on every writing action, and a cookie scoped to the admin section with httponly and secure set from the actual protocol.
  • The CRM webhook is protected by a secret in the URL — amoCRM webhooks come with no signature.
  • Graceful degradation everywhere: no config — the template is used; no minified file — the source; CRM unreachable — Telegram takes over; nothing worked at all — the request goes into the log.

What makes it interesting

  • 80 city versions of the site without a single copy of the files — substituting grammatical cases through the output buffer turned out to be both simpler and more reliable than template functions.
  • Integration with somebody else's live CRM with no staging environment: a field map generated by a script from an account dump, duplicate merging by the last ten digits of the phone number, and a local dry run of the request layout without a single outbound call.
  • A request is never lost: CRM → Telegram → a log on disk, counted as a success if any link in the chain works.
  • 100/100/96/100 on a service page with an entirely hand-written stack, no CDN, no cache layer and no bundlers — on cheap shared hosting.
  • The full cycle by one person: content migration from Tilda → architecture → backend → frontend → admin panel → CRM integration → preparing 100+ images → SEO → optimisation → deployment to the production domain.
Business site

PriceSmeta — digital goods marketplace

RoleSole developer — full-stack: architecture, PHP/MySQL backend, frontend, user accounts and payments, admin panel, SEO and deployment
StackPHP · MySQL · HTML · CSS · JavaScript — no frameworks or CMS

PricesMeta, a hand-built digital goods platform: ready-made automated cost estimates and Excel price lists for electricians, plumbers, tilers, crews and construction companies. A complete product on PHP + MySQL with no CMS or frameworks — catalog, product pages, user accounts, online payment with file delivery, an articles section, FAQ and a custom admin panel.

What was neededSell ready-made Excel cost estimates so that the buyer pays and gets the file immediately — with no email exchange and no sending by hand.

What came out of itPayment and file delivery run on their own. The buyer gets an account with purchase history, the owner an admin panel with statistics.

What's inside

The full lifecycle of a digital product — from the storefront to file delivery after payment.

  • A digital goods catalog and product pages; the homepage pulls the latest publications from the database.
  • User account: registration, login, password recovery.
  • Online payment with a signed transaction (HMAC) and automatic file delivery after purchase.
  • An articles section (blog) and FAQ, terms of offer and a data processing policy.

Architecture

Server-side rendering in PHP with a MySQL database (PDO), no CMS or frameworks.

  • Products and articles are stored in the database and publishing is controlled by a flag — only published items reach the site.
  • The code is split into reusable includes (header, footer, popups) and navigation helpers.
  • Sessions for authentication, with protection on service pages and the payment flow.

Custom admin panel

An entirely hand-written panel for managing content and sales: the product catalog, articles (publish and unpublish) and order handling.

Accessibility and SEO

  • A semantic main landmark, proper contrast and accessible navigation.
  • Keyword-first title, meta description, Open Graph and meaningful markup.

What makes it interesting

  • The largest of the business projects: a full product platform with user accounts, payments and an admin panel — on plain PHP + MySQL, without a CMS.
  • The full cycle by one person: architecture → backend and database → frontend → payments → admin panel → SEO → deployment.
Business site

Metalworking — corporate website in 9 languages

RoleSole developer: design, frontend, form backend, multilingual support (9 languages), SEO and deployment
StackPHP · HTML · CSS · JavaScript — no frameworks or CMS

A corporate site for a metalworking company: turning and milling of parts to drawing, prototypes and production runs, serving customers across Europe. The goal was to show the manufacturing capabilities clearly and collect quote requests with attached drawings in nine languages at once. A multi-section site on a pure stack, no CMS or frameworks.

What was neededShow manufacturing capability to European buyers and take enquiries with drawings attached — in nine languages at once.

What came out of itNine language versions on one site, the form accepts drawings as files, and each enquiry arrives tagged with its language and page.

What's inside

A multi-section structure focused on manufacturing capabilities.

  • Hero and a company metrics block: years of experience, machine park, completed orders.
  • An equipment slider and a portfolio of completed projects (a slider on desktop, a grid on mobile).
  • Services and types of milling operations, with an explanatory video.
  • A modal “Get a quote” form and scroll-triggered section reveal animations.

A quote form built for manufacturing

The main working element is accepting a request with technical drawings, not just “name + phone”.

  • Upload of up to 10 files in manufacturing formats (DXF, DWG, PDF, ZIP, images, documents), sent by email with attachments.
  • Three layers of spam protection: a server-side arithmetic captcha, a honeypot and a timer trap against instant bot submissions.
  • Validation on both client and server, submitted over AJAX without reloading the page.

Nine languages

The site is fully translated into nine languages: English (primary), Russian, German, French, Italian, Spanish, Polish, Romanian and Serbian.

  • Each language has its own content and its own server-side form handler.
  • A custom language switcher with flags.
  • hreflang + x-default across all nine versions.

Accessibility

  • A semantic main landmark and aria labels on interactive elements (burger menu, language switcher).
  • A correct heading hierarchy and meaningful alt text.
  • WCAG-compliant contrast and focus navigation.

What makes it interesting

  • A serious B2B site with a complex form built around a real manufacturing process — accepting drawings in engineering formats.
  • Full multilingual support across nine locales on a pure stack, with no i18n frameworks.
  • The full cycle by one person: design → markup → form backend → localisation → deployment.

What next

Timelines and price ranges for a site like this are on the how much a website costs page. If one page is enough for your case, that is the landing page section.