50 days of SEO in B2B cleaning: 194 pages in Yandex and +86% GSC clicks

50 working days from the site relaunch: 194 pages indexed in Yandex, +86% GSC clicks, Lighthouse 93/100. Zero paid links, zero agency. Week-by-week.

On March 22, 2026 I shipped a new website for the cleaning company where I work as in-house head of marketing. Fifty working days later: 194 pages in the Yandex index, +86% clicks and +90% impressions month-over-month in Google Search Console, Lighthouse Performance 93/100 on mobile, CLS 0.002. Zero paid links, zero agency, zero contractors. Custom-built PHP 8.4 site, 245 URLs in the sitemap.

The short version

  • 245 URLs in 50 days: 51 service pages, 18 St Petersburg districts, 70 metro stations, 12 satellite cities, 50+ blog posts.
  • 194 pages in Yandex Searchable. Started at 111 on April 13, reached 194-201 by May 5 (it fluctuates).
  • 92 pages recovered from Yandex ‘Excluded by noindex’ status via recrawl API + IndexNow + lastmod.
  • +86% GSC clicks and +90% impressions month-over-month (52 → 97 clicks, 2,826 → 5,359 impressions).
  • Lighthouse mobile: Performance 93, SEO 100, CLS 0, LCP 2.4s, TBT 60ms.
  • Internal SEO Health Score 62 → 84 in 18 days.

Below: what I did week by week, which fixes actually moved the numbers, and where I tripped.

The starting point on March 22

March 22 was the initial commit on an empty custom PHP project. No CMS, no build pipeline, no node_modules. Six system pages (home, pricing, about, contacts, licenses, privacy) and 18 service pages. Everything else lay ahead.

I chose a deliberately simple stack:

  • PHP 8.4 on shared hosting, no CMS
  • .htaccess as the router, pattern /uborka-metro-{slug}/uborka-metro-{slug}.php
  • MySQL for blog, leads, and dynamic AggregateRating
  • CSS directly in the repo, vanilla JS, no bundler, no transpiler
  • Self-hosted Inter (woff2), no Google Fonts

Why this stack. The company already had a website - old and heavy. I could not afford a six-month migration to a ‘modern stack’. I needed a fast start, control over every byte of markup, and the ability to edit schema without fighting CMS plugins. Custom PHP closes all three and delivers HTML in milliseconds.

What I did each week

Week 1-2: technical foundation and forms

First thing was form anti-spam. Six filters, no captcha: a honeypot <input name="website" style="display:none">, regex for URLs in the comment, phone length 10-11 digits, non-Cyrillic ratio check on comments over 5 characters, rate-limit at 5 requests per hour per IP, log to a protected spam_log.txt. Captcha adds +5 to user friction; these filters kill 99% of spam without captcha.

Then CSP with a whitelist for GA4, Yandex Metrica, Roistat, AmoCRM, Beget CDN. HSTS preload, Referrer-Policy, X-Frame-Options. CSRF tokens on every POST.

All analytics wrapped in requestIdleCallback(fn, {timeout: 3000}) - GA4, Metrica, and Roistat do not block render. Preconnect for analytics did not work - the connection expired before use, wasting resources. dns-prefetch was enough.

By the end of week 2: self-hosted Inter with @font-face size-adjust: 107%; ascent-override: 90% based on Arial - killed the FOUT shift that was eating into CLS.

Week 3: first audit, mapping the site

April 5: first full SEO audit with 7 parallel sub-audits: content / E-E-A-T, technical SEO, on-page, schema, Core Web Vitals, AI search readiness, images. Weights: 23/22/20/10/10/10/5%. Score 62/100. Main gaps: schema (LocalBusiness duplicated across 30+ pages), CLS 0.377, inline XSS in js/main.js, MD5 admin password.

By April 8 everything closed: one LocalBusiness multi-type block with @id in the footer, page-level references via @id instead of repetition; CLS 0.377 → 0.002 in a day; XSS fixed via createElement + textContent; admin password moved to SHA-256 + salt. By April 10 the score was 72.

In parallel I ran the site through Screaming Frog and found 474 URLs: 245 of them were what I wanted, the rest were stale versions and template stubs. I pulled 43 metro pages with thin content (~230 words) out of the sitemap.

Week 4: Programmatic SEO - 100 pages

From April 16 was programmatic day: I filled 39 metro stations + 18 districts + 12 satellite cities with unique content. One template, unique content - $district_content, $metro_content, $city_content injected into a shared metro-template.php, district-template.php, lo-template.php. The uniqueness threshold per page: above 45%, checked via Screaming Frog Near Duplicates.

To avoid doorway risk, every page got:

  • A unique H1 and H2 (no template lines like ‘Cleaning in district X’)
  • A ‘Local features’ section of 200-300 words about the specific district or metro
  • 3-5 location-specific FAQs
  • Prices, dispatch time for the cleaning crew, address of the nearest active company site

My internal Programmatic SEO Score landed at 95/100. That is about the template, not the actual ranking - I will be checking impressions in GSC in 3-4 months.

Week 5: blog wave

April 9: blog wave one, 21 posts shipped in a day. April 17: wave two, 10 more. By end of April there were 50+ posts in the blog, 1,500-4,000 words each, every one with:

  • BlogPosting + FAQPage + QAPage schema
  • 4-5 FAQ questions, real queries pulled from Wordstat and People Also Ask
  • 3+ internal links: one to a service, one to another post, one to /price/ or contacts
  • An Author Person with rel="author" on dimaserbun.ru
  • A Pexels banner (free license), plus a separate JPEG version for Yandex Zen

Cadence after the two waves: 2-3 posts per week. Do not publish more than 5 in a day and do not backdate 50 into a single month - that burns the search engine’s filter templates. On May 6 I redistributed 11 ready posts across April 20-30 so the publication density looked even.

Week 6: AI citability and Excluded recovery

April 24-26, three streams running in parallel.

Excluded recovery. April 13: 56 pages in Yandex Excluded. April 22: 92. Half the site. Through Yandex Webmaster API → /recrawl/queue I sent 37 blog posts for recrawl (quota: 620 URLs/day) plus IndexNow ping for 70 metro URLs. By May 5: 1 page left in Excluded out of 93. Most came back to Searchable, now 194-201.

GEO audit and AI Overviews. Platform scores: Google AIO 58, ChatGPT web search 48, Perplexity 62, Bing Copilot 64. Total 61/100. Main fixes: 134-167 word passages under H2 on 5 key pages with the direct answer in the first 40-50 words + a .answer-block class for speakable; H2 rephrased as a question (‘What cleaning suits your apartment type?’ instead of ‘Cleaning by apartment type’); AI crawlers allowed in robots.txt - GPTBot, OAI-SearchBot, ClaudeBot, anthropic-ai, Google-Extended, PerplexityBot, CCBot, YouBot, cohere-ai, Bytespider, meta-externalagent. Plus llms.txt at the site root in llmstxt.org format - H1, blockquote description, H2 sections with lists, business registration / ISO / ratings.

Glossary. April 26: /glossary/ page with 45 cleaning industry terms + DefinedTermSet schema. Internal links - 188 references to /glossary/#anchor from site copy - this sends a structural signal ‘this site has its own terminology’ and opens the door for AI citations.

Week 7: Tier A and Tier B, template expansion

May 5: trust and YMYL pack.

Tier A (trust): preconnect to the CDN domain, mobile preload of the hero image, dynamic AggregateRating from MySQL in schema, an AreaServed array of 31 Cities (18 St Petersburg districts + 12 Leningrad Oblast + St Petersburg itself). Removed brand names from page body where they were not strictly factual - Karcher, Koch Chemie, others - kept only where they are genuine product facts.

Tier B (YMYL): on the death-cleaning and post-fire cleanup pages - links to the Rospotrebnadzor license, SanPiN 3.3686-21, 1.2.3685-21, 2.1.3684-21, hyperlinked to consultant.ru. For YMYL niches this is critical: Google and Yandex penalize pages without authoritative confirmation hard.

May 6: expanded the uborka-N-komnatnoj templates via competitor SERP analysis: 1,500 → 1,700-1,900 words, FAQ 5 → 10. Expanded the metro template: FAQ 3 → 5, added a ‘Preparation’ section, SEO text. Recrawled 70 URLs via IndexNow.

May 7: a sync check across GSC and Yandex Webmaster. Sent 39 URLs to recrawl, fixed the Yandex Zen RSS feed (removed duplicate h1, converted tables to lists).

The five fixes that actually moved the metrics

Picking the five decisions with the biggest impact:

DecisionImpact
One LocalBusiness block with @id in the footerSchema no longer duplicated on 30+ pages, Rich Results Test clean
Self-hosted Inter with size-adjust:107% fallbackCLS 0.377 → 0.002 in a day
Programmatic 100 district/metro pages with 45%+ uniqueness+44 pages in Yandex index in 11 days (111 → 155)
Yandex Webmaster API recrawl + IndexNow + lastmodRecovered 92 out of 93 pages from Excluded
134-167 word passages with the direct answer in first 40-50Position lift on ‘cleaning apartments St Petersburg’ cluster: avg 28.8 → 16-24

The numbers

MetricStart (March)April 24May 5
GSC clicks/month5297 (+86%)growing
GSC impressions/month2,8265,359 (+90%)up to 595/day
GSC average position28.827.316-24
Yandex Searchable111 (Apr 13)155194-201
Yandex Excluded56 (Apr 13)921
CLS0.377-0
LCP mobile~4.5s-2.4s
Lighthouse Performance~60-70-93/100
SEO Health Score6272 (Apr 10)84 (Apr 23)

Top Yandex queries by impressions as of May 7: ‘cleaning services St Petersburg prices’ - 103 impressions, ‘industrial cleaning price per square meter’ - 79, ‘cleaning cost’ - 77, ‘cleaning St Petersburg’ - 72. By clicks: brand queries in top-3, plus ‘industrial enterprise cleaning’ and ‘underground parking cleaning’ (27 impressions at position 6 after the H2 and FAQ rewrite).

What is next

Open items at the end of the 50-day stretch:

  • YouTube channel + 5 videos - correlation with AI citations 0.737, the strongest signal
  • Wikidata entry for the company
  • Reddit - 10-15 expert answers per month
  • LinkedIn Company Page
  • 6 first-wave directories: Cleannow, Orgpage, Cataloxy, Rosfirm, Spr, Profi.ru
  • 1-2 publications in St Petersburg business outlets → then Wikipedia ru
  • llms-en.txt - English version for global AI

The takeaway. A new site can reach a state where Yandex holds 200 pages in the index and GSC clicks double - in 50 days, with no link budget. The conditions: everything by hand, no off-the-shelf CMS theme for the niche, direct control over schema and performance, blog cadence discipline, recrawl via API.

If you run a B2B site in a local-services niche and need structural SEO traction, message me on Telegram @dimik90.

Related: how I work - no fluff, no 40-slide proposals.

Frequently asked questions

What does SEO for a cleaning company in St Petersburg cost from scratch?
In this case, the budget for paid links and SEO agencies was zero. I run marketing in-house as head of marketing, and I also write the code, content, schema, and internal links. Tools are a standard kit: Google Search Console, Yandex Webmaster, Yandex Metrica, Wordstat, Screaming Frog (freelance license), a few MCP servers. As a benchmark for comparison: an agency offering 'SEO for services from 50,000 rubles per month' will cover the technical part, content, and regular updates - that is the St Petersburg market in 2026.
How long before a new site sees first results in Yandex?
First impressions and clicks started coming in week 2-3 after launch, but those were near-brand queries. Real growth on niche keywords started in week 5-6, after I shipped the programmatic cluster of 100 district and metro pages and a wave of 21 blog posts. By day 50: 194 pages in Yandex Searchable, +86% GSC clicks. This is for a new domain with no history and no paid backlinks.
What should you do first - links or content?
Neither, in my case. The first thing is technical: form anti-spam, correct schema, clean sitemap, sane title and description tags, CLS in the green zone, AI crawlers allowed in robots.txt. Without that, links and content pour into a leaky bucket. Content I built up in parallel with the technical work, and link building I left for later - directories at one per day, plus 5 'best of' mentions in niche industry rankings.
Can the same approach work for a different niche, not cleaning?
The approach yes, the numbers no. The programmatic template for districts and metro works in local-services niches: cleaning, repair, delivery, disinfection, logistics. The blog cadence of 2-3 posts per week is universal. The checklist for performance and schema is general. But query volumes and competition density vary by niche, so you have to find the growth points in your own keyword research.
Which tools delivered the most?
Yandex Webmaster API for recrawl (quota: 620 URLs/day) - brought 92 pages back from Excluded to Searchable. IndexNow - Bing and Yandex see a new URL in hours instead of weeks. Screaming Frog - found 43 thin pages I removed from sitemap, rewrote, and put back. Wordstat API - built a 861-phrase keyword set across 20 clusters. Lighthouse - kept running until everything was in the green zone after each batch of fixes. Without these, I would have been working blind.