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
.htaccessas 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:
| Decision | Impact |
|---|---|
One LocalBusiness block with @id in the footer | Schema no longer duplicated on 30+ pages, Rich Results Test clean |
| Self-hosted Inter with size-adjust:107% fallback | CLS 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 + lastmod | Recovered 92 out of 93 pages from Excluded |
| 134-167 word passages with the direct answer in first 40-50 | Position lift on ‘cleaning apartments St Petersburg’ cluster: avg 28.8 → 16-24 |
The numbers
| Metric | Start (March) | April 24 | May 5 |
|---|---|---|---|
| GSC clicks/month | 52 | 97 (+86%) | growing |
| GSC impressions/month | 2,826 | 5,359 (+90%) | up to 595/day |
| GSC average position | 28.8 | 27.3 | 16-24 |
| Yandex Searchable | 111 (Apr 13) | 155 | 194-201 |
| Yandex Excluded | 56 (Apr 13) | 92 | 1 |
| CLS | 0.377 | - | 0 |
| LCP mobile | ~4.5s | - | 2.4s |
| Lighthouse Performance | ~60-70 | - | 93/100 |
| SEO Health Score | 62 | 72 (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.