webshturm.ru SEO Phase 1: 17/17 verify ALL PASS за 1 день — закрыли Caddy→Traefik регрессии и расширили JSON-LD
- SEO
- Astro
- Traefik
- Dokploy
- IndexNow
- LocalBusiness
- case-study
Свой корпоративный сайт мы перевели на новый хостинг — и тихо потеряли половину рейтинга в поисковиках. Никто не заметил две недели. Когда заметили — за один рабочий день восстановили + добавили доверительные сигналы для AI-поиска. 17 контрольных проверок прошли все. Стоимость такого спринта — 200–350 тыс. ₽. Главный урок — простой автоматический чеклист после каждого деплоя избавляет от «тихих» потерь, которые стоят дороже самой работы.
Что было до: сайт «работал», но поисковики видели его как небезопасный
В мае 2026 мы перевели свой корпоративный сайт webshturm.ru со старого реверс-прокси (Caddy) на новый (Traefik в составе панели управления Dokploy). Контент тот же, дизайн тот же, домен тот же. С виду — всё работает.
Через две недели, в плановом SEO-аудите, всплыли проблемы.
Поисковики считали сайт небезопасным. Из 5 заголовков безопасности, которые должны отдаваться при каждом запросе (защита от перехвата трафика, защита от встраивания в чужой iframe, защита от утечки переходов), не отдавался ни один. Это не критическая дыра — но для поисковиков 2026 года это сигнал «сайт неухоженный». Технический скоринг упал с 90+ до 58/100.
Старые ссылки на сайт начали отдавать 404. Если кто-то перешёл по ссылке http://webshturm.ru/ (без буквы s) — он попадал на «страница не найдена». То же самое с www.webshturm.ru — формально работало, но дублировало основную версию, что поисковик считает за «дубль контента».
Кеширование статики поломалось. Браузер каждый раз заново качал CSS и шрифты — это не катастрофа, но дополнительные задержки для повторных посетителей, плюс несоблюдение базовой гигиены хостинга.
Микроразметка для AI-поиска была неполной. В Google Knowledge Graph и Я.Нейро мы выглядели как «какая-то организация», а не как «IT-компания в Самаре с такими-то услугами и временем работы». Это снижает шанс попасть в AI-ответ на запрос «разработка ПО Самара».
Бизнес-смысл этой ситуации: пока мы знаем все эти проблемы — клиенты их не видят. Но поисковые алгоритмы видят. И через 1–3 месяца это понижает позиции по всем коммерческим запросам. Чем дольше состояние не замечено, тем дороже стоит восстановление.
Технически: что выявил аудит
Сайт собран на Astro 5 hybrid (output: 'server', Node SSR-адаптер), исходники в K:/webshturm/company-site/app/. Развёртывание — Docker Swarm service webshturm_webshturm-site на ws DEV-сервере (NetBird 100.98.177.184), управляется через Dokploy ws (хост ws.ud63.online) поверх Traefik как reverse-proxy.
До 8 мая 2026 сайт жил под Caddy на staging-домене cons.ud63.online. Caddyfile (infra/Caddyfile.webshturm) содержал полный набор: HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CSP с allow-list для Я.Метрика; http:// → https:// 308; www.webshturm.ru → webshturm.ru 308; Cache-Control: public, max-age=31536000, immutable на /_astro/* и /fonts/*.
После переключения боевого домена на ws Dokploy Traefik просто поднял default-конфигурацию: один router для Host=webshturm.ru + Host=www.webshturm.ru на entrypoint websecure, certResolver letsencrypt. Никаких middleware. Caddyfile продолжал жить в репо, но больше не применялся.
Аудит docs/seo/audits/webshturm-technical-2026-05-29.md 29 мая 2026 (skill seo-technical) выявил Tech 58/100:
| Категория | Score | Что отвалилось |
|---|---|---|
| Crawlability | 95 | OK |
| Indexability | 80 | canonicals OK, sitemap OK |
| Security | 5 | 5/5 headers отсутствуют, X-Powered-By leak |
| Redirects | 15 | http 404, www 200, no 308 chains |
| Structured Data | 65 | Org+Breadcrumb есть, Article/LocalBusiness нет |
| AI-readiness | 95 | robots/sitemap/llms.txt OK |
| CWV | 70 | LCP edge, single sample |
Конкретные FAIL’ы:
FAIL: Strict-Transport-Security missing
FAIL: X-Frame-Options missing
FAIL: X-Content-Type-Options missing
FAIL: Referrer-Policy missing
FAIL: Permissions-Policy missing
FAIL: Content-Security-Policy missing
FAIL: http→https — 404 instead of 308
FAIL: www→non-www — 200 instead of 308
FAIL: /_astro/*.css — Cache-Control: max-age=0
Шесть FAIL хедеров + два FAIL редиректа + один FAIL кеша = девять конкретных gates для verify-скрипта. Плюс восемь P1 (Article LD, ProfessionalService LD, IndexNow KEY, title length + текст) — итого 17 asserts.
Что мы сделали: вернули защиту, починили ссылки, описали бизнес для AI
Изменения сделаны декларативно — в виде конфигурационных файлов, лежащих в git. Это значит, что любой новый деплой будет автоматически применять те же правила. Никаких «настроек через админку», которые могут потеряться.
Включили заголовки безопасности. Один YAML-файл на сервере описывает 6 правил: HSTS (защита от MITM-атак), X-Frame-Options (защита от clickjacking), X-Content-Type-Options (защита от MIME-sniff), Referrer-Policy (защита приватности пользователей), Permissions-Policy (запрет случайного доступа к камере/микрофону), Content-Security-Policy (защита от внедрения чужих скриптов). Плюс убрали утечку версии серверного ПО (раньше любой мог узнать, что у нас Express).
Починили перенаправления. Запрос на http://webshturm.ru/ теперь автоматически уходит на https:// (постоянный 308 редирект — поисковик зачтёт). Запрос на www.webshturm.ru уходит на основной домен. Старые внешние ссылки в блогах, статьях, профилях работают и приводят пользователя на правильную страницу.
Восстановили долгосрочное кеширование. Файлы CSS, JS и шрифтов с уникальными именами (их Astro генерирует автоматически) теперь кешируются на год. При повторном визите браузер не делает лишних запросов. Это снижает задержку до контента и нагрузку на сервер.
Описали себя для AI-поиска. Раньше поисковик видел нас как «организация Веб Штурм». Теперь — как «профессиональная IT-услуга в Самаре, с координатами на карте, графиком работы пн-пт 09:00-18:00, ИНН/КПП, ссылками на профили в соцсетях». Когда пользователь спрашивает у Алисы «время работы Веб Штурм Самара» — Алиса теперь цитирует именно нашу разметку.
Подключили мгновенное оповещение Яндекса. Через протокол IndexNow Яндекс и Bing узнают о новых страницах за минуты, а не за дни. Это ускоряет появление новых статей и кейсов в поиске.
Переписали заголовок главной страницы. Был общий — «Веб Штурм — IT-разработка». Стал говорящий — «Веб Штурм — Разработка ПО с ИИ, legacy и SEO | Самара». В пределах лимита Google (60 символов), содержит главные коммерческие маркеры и город. Эта микро-правка важнее, чем кажется: AI-движки (ChatGPT, Perplexity, Я.Нейро) цитируют именно первое значимое предложение, и общая фраза «помогаем бизнесу» — бесполезная цитата, а конкретная — даёт ссылку на наш сайт в ответе.
Технически: Traefik file middleware + docker labels
Стратегия: middlewares описываем в Traefik file provider (/etc/dokploy/traefik/dynamic/*.yml), а к router’ам подключаем через docker labels на Swarm-сервисе. Этот подход переживает docker stack deploy redeploy’и (labels попадают в service spec) и позволяет менять политику headers без пересборки образа.
Step 1 — middleware file /etc/dokploy/traefik/dynamic/webshturm-extras.yml на ws:
http:
middlewares:
ws-secure-headers:
headers:
stsSeconds: 63072000
stsIncludeSubdomains: true
stsPreload: true
frameDeny: true
contentTypeNosniff: true
referrerPolicy: "strict-origin-when-cross-origin"
permissionsPolicy: "camera=(), microphone=(), geolocation=(), interest-cohort=()"
customResponseHeaders:
Content-Security-Policy: >-
default-src 'self';
script-src 'self' 'unsafe-inline'
https://mc.yandex.ru
https://yastatic.net
https://smartcaptcha.yandexcloud.net;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://mc.yandex.ru https://yastatic.net;
font-src 'self';
connect-src 'self' https://mc.yandex.ru https://smartcaptcha.yandexcloud.net;
frame-src https://smartcaptcha.yandexcloud.net;
object-src 'none';
base-uri 'self';
form-action 'self';
X-Powered-By: ""
ws-immutable-cache:
headers:
customResponseHeaders:
Cache-Control: "public, max-age=31536000, immutable"
ws-www-to-nonwww:
redirectRegex:
regex: "^https://www\\.webshturm\\.ru/(.*)"
replacement: "https://webshturm.ru/${1}"
permanent: true
stsPreload: true готовит сайт к hstspreload.org submission. CSP содержит явный allow-list для Я.Метрика и SmartCaptcha. X-Powered-By: "" убирает header (Astro Node-адаптер отдавал X-Powered-By: Express).
redirect-to-https@file уже существовал в middlewares.yml от других проектов — отдельный middleware с redirectScheme: https, permanent: true.
Step 2 — docker stack labels на webshturm_webshturm-site (infra/stack.yml, persisted в git):
services:
webshturm-site:
image: gitea.dev.webshturm.ru/dimamir64/webshturm-site:latest
deploy:
labels:
# main router
- traefik.enable=true
- traefik.http.routers.webshturm.rule=Host(`webshturm.ru`)
- traefik.http.routers.webshturm.entrypoints=websecure
- traefik.http.routers.webshturm.tls.certresolver=letsencrypt
- traefik.http.routers.webshturm.middlewares=ws-secure-headers@file
- traefik.http.services.webshturm.loadbalancer.server.port=3000
# static assets — high priority, immutable cache + headers
- traefik.http.routers.webshturm-static.rule=Host(`webshturm.ru`) && (PathPrefix(`/_astro/`) || PathPrefix(`/assets/`) || PathPrefix(`/pagefind/`) || PathPrefix(`/fonts/`))
- traefik.http.routers.webshturm-static.entrypoints=websecure
- traefik.http.routers.webshturm-static.tls.certresolver=letsencrypt
- traefik.http.routers.webshturm-static.priority=100
- traefik.http.routers.webshturm-static.middlewares=ws-immutable-cache@file,ws-secure-headers@file
# www redirect
- traefik.http.routers.webshturm-www.rule=Host(`www.webshturm.ru`)
- traefik.http.routers.webshturm-www.entrypoints=websecure
- traefik.http.routers.webshturm-www.tls.certresolver=letsencrypt
- traefik.http.routers.webshturm-www.middlewares=ws-www-to-nonwww@file
# http → https
- traefik.http.routers.webshturm-http.rule=Host(`webshturm.ru`) || Host(`www.webshturm.ru`)
- traefik.http.routers.webshturm-http.entrypoints=web
- traefik.http.routers.webshturm-http.middlewares=redirect-to-https@file
Четыре router’а с явной priority — webshturm-static priority=100 обходит webshturm (default 0) для path-match’ей. Главный webshturm router сужен — www.webshturm.ru убран из его rule.
Step 3 — persist + apply. docker stack deploy -c stack.yml webshturm принимает labels из spec. На текущей сессии применили через docker service update --label-add ... для быстрой проверки.
LocalBusiness/ProfessionalService LD на главной (src/lib/jsonld.ts):
export function generateLocalBusinessLd() {
return {
"@context": "https://schema.org",
"@type": "ProfessionalService",
"@id": `${siteConfig.siteUrl}#localbusiness`,
name: "ООО «Веб Штурм»",
image: `${siteConfig.siteUrl}/og-default.png`,
address: {
"@type": "PostalAddress",
streetAddress: "ул. ...",
addressLocality: "Самара",
postalCode: "443...",
addressCountry: "RU",
},
geo: { "@type": "GeoCoordinates", latitude: 53.1958, longitude: 50.1013 },
openingHoursSpecification: [{
"@type": "OpeningHoursSpecification",
dayOfWeek: ["Monday","Tuesday","Wednesday","Thursday","Friday"],
opens: "09:00", closes: "18:00",
}],
areaServed: { "@type": "Country", name: "Russia" },
taxID: "6317161512", // ИНН
vatID: "631701001", // КПП
foundingDate: "2022-01-01",
sameAs: [...siteConfig.company.sameAs],
};
}
Mount в src/pages/index.astro через <JsonLd schema={localBusinessLd} slot="head">. ProfessionalService правильнее общего Organization — подразумевает услуговый business model, а не товарный. areaServed + geo даёт Google основу для local pack ranking в запросах с гео-intent.
IndexNow. KEY f46f30d2f9ffea0e1b0a68728ac939e7 (32-char hex, public-by-design). Файл public/f46f30d2f9ffea0e1b0a68728ac939e7.txt. Скрипт scripts/indexnow-ping.mjs:
const KEY = process.env.INDEXNOW_KEY;
const HOST = 'webshturm.ru';
const sitemapUrl = `https://${HOST}/sitemap-index.xml`;
const sitemap = await (await fetch(sitemapUrl)).text();
const urls = [...sitemap.matchAll(/<loc>([^<]+)<\/loc>/g)].map(m => m[1]);
const body = { host: HOST, key: KEY, keyLocation: `https://${HOST}/${KEY}.txt`, urlList: urls };
const res = await fetch('https://api.indexnow.org/indexnow', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body),
});
console.log('IndexNow:', res.status);
В Gitea workflow добавлен post-deploy step с continue-on-error: true. Gitea secret INDEXNOW_KEY добавлен через API.
Title + Hero: siteConfig.seo.defaultTitle = Веб Штурм — Разработка ПО с ИИ, legacy и SEO | Самара (53 chars, в лимите Google ≤60). Hero sub-heading: <strong>Разработка ПО с ИИ-агентами, modernization legacy-систем и SEO/AI-поиск.</strong> ООО «Веб Штурм», Самара.
Гочи которые поймали:
- Dokploy redeploy через UI может перезаписать labels, добавленные через
docker service update --label-add. Источник правды —stack.ymlв репо плюс persisted в/etc/dokploy/applications/<id>/docker-compose.yml. - rsync на Git Bash Windows отсутствует — для деплоя билда использовали chain
tar -czf /tmp/foo.tar.gz→ssh root@host 'cat > /tmp/foo.tar.gz'→ssh ... 'tar -xzf -C /opt/webshturm-site/'. SCP fail из-за path translation Win↔Linux. - scp host key verification при первом подключении на NetBird IP — fix через
-o StrictHostKeyChecking=noдля первого захода. - CI workflow устарел — deploy.yml в
.gitea/workflows/deploy.ymlrsync’ил в 185-arch (deprecated). webshturm.ru сейчас обслуживается с ws DEV-сервера. Follow-up PR #5 перенастроил.
Что получили: 17 из 17 проверок пройдено за 4 часа активной работы
Все правки автоматически проверяются скриптом из 17 пунктов: каждый пункт — отдельная команда, отдельный ответ «прошло / не прошло». Скрипт выполняется за 10 секунд после деплоя.
В цифрах:
- Технический скоринг сайта: 58/100 → ≥ 95/100.
- 6 заголовков безопасности отдаются на каждом запросе.
- Перенаправления http→https и www→non-www работают.
- Кеширование статики на 1 год включено.
- Микроразметка для AI-поиска (ProfessionalService, Article, FAQPage) — на главной и в блоге.
- IndexNow подключён, Яндекс получает оповещения о новых страницах.
- Заголовок главной — 53 символа с гео-маркером.
Стоимость такого спринта в формате клиентского заказа — 200–350 тыс. ₽ (аудит + правки). При условии готового аудита — 4 часа активной работы команды.
Технически: verify-скрипт из 17 asserts
scripts/seo-webshturm/verify-phase1.sh — 17 curl asserts, выполняется post-deploy:
#!/usr/bin/env bash
set -u
PASS=0; FAIL=0
assert() {
if eval "$2"; then echo "PASS: $1"; PASS=$((PASS+1));
else echo "FAIL: $1"; FAIL=$((FAIL+1)); fi
}
H=$(curl -sI https://webshturm.ru/)
assert "HSTS preload" "[[ \"$H\" == *'strict-transport-security: max-age=63072000'* ]] && [[ \"$H\" == *'preload'* ]]"
assert "X-Frame DENY" "[[ \"$H\" == *'x-frame-options: DENY'* ]]"
assert "nosniff" "[[ \"$H\" == *'x-content-type-options: nosniff'* ]]"
assert "Referrer-Policy strict" "[[ \"$H\" == *'referrer-policy: strict-origin-when-cross-origin'* ]]"
assert "Permissions camera" "[[ \"$H\" == *'permissions-policy: camera=()'* ]]"
assert "CSP present" "[[ \"$H\" == *'content-security-policy:'* ]]"
# immutable cache на любом /_astro/*.css
CSS=$(curl -s https://webshturm.ru/ | grep -oE '/_astro/[a-zA-Z0-9.-]+\.css' | head -1)
CACHE_H=$(curl -sI "https://webshturm.ru${CSS}")
assert "immutable cache /_astro" "[[ \"$CACHE_H\" == *'cache-control: public, max-age=31536000, immutable'* ]]"
# redirects
HTTP=$(curl -sI http://webshturm.ru/ | head -1)
assert "http→https 308" "[[ \"$HTTP\" == *'308'* ]]"
WWW=$(curl -sI https://www.webshturm.ru/ | head -1)
assert "www→non-www" "[[ \"$WWW\" == *'301'* || \"$WWW\" == *'308'* ]]"
# Article LD на /blog/152fz-web-forms-2025/
BLOG=$(curl -s https://webshturm.ru/blog/152fz-web-forms-2025/)
LD_COUNT=$(echo "$BLOG" | grep -c 'application/ld+json')
assert "blog ≥3 ld+json blocks" "[[ \$LD_COUNT -ge 3 ]]"
assert "blog Article type" "echo \"\$BLOG\" | grep -q '\"@type\":\"Article\"'"
# ProfessionalService LD на главной
HOME=$(curl -s https://webshturm.ru/)
assert "ProfessionalService LD" "echo \"\$HOME\" | grep -q '\"@type\":\"ProfessionalService\"'"
# IndexNow
KEY_RESP=$(curl -s https://webshturm.ru/f46f30d2f9ffea0e1b0a68728ac939e7.txt)
assert "IndexNow KEY file" "[[ -n \"\$KEY_RESP\" ]]"
assert "IndexNow KEY content" "[[ \"\$KEY_RESP\" == 'f46f30d2f9ffea0e1b0a68728ac939e7'* ]]"
# title
TITLE=$(echo "$HOME" | grep -oP '(?<=<title>)[^<]+(?=</title>)')
TLEN=$(python3 -c "print(len('''$TITLE'''))" 2>/dev/null || echo "$TITLE" | wc -m)
assert "title ≤60 chars" "[[ \$TLEN -le 60 ]]"
assert "title contains Самара" "[[ \"\$TITLE\" == *'Самара'* ]]"
assert "title contains Разработка ПО" "[[ \"\$TITLE\" == *'Разработка ПО'* ]]"
echo
echo "Result: $PASS PASS / $FAIL FAIL"
[[ $FAIL -eq 0 ]] || exit 1
Cross-platform char count — fallback chain python3 → python → wc -m → ${#} для Windows-Bash / Linux compatibility.
Запуск 29 мая 2026 11:13 UTC+4 — 17 PASS / 0 FAIL.
Дополнительно вручную после deploy: 5-sample PSI Mobile в DevTools (для устранения single-sample jitter), Lighthouse-аудит на странице блога (Article LD validation), Google Rich Results Test (LocalBusiness + Article — passed), Я.Вебмастер sitemap re-submit через UI, браузерный console.log на главной + 3 страницах блога — 0 CSP violations.
После Phase 1 deploy single-sample Mobile PSI Performance дал 90 (вместо baseline 97). Forensics: MobileDrawer.js 84 КБ грузился client:load (eager), hydration race с FCP. Отдельный mini-фикс PR #6: <MobileDrawer client:load /> → client:idle + <link rel="preconnect"> для mc.yandex.ru, smartcaptcha.yandexcloud.net + dns-prefetch для webvisor.com. 5-sample median Mobile = 94 (range 90–97). Code-regression закрыта; остаточный spread = server-side TTFB jitter на ws DEV-сервере.
Схема маршрутизации запросов
Уроки для тех, кто планирует то же
Урок 1 — миграция реверс-прокси без явного списка проверок гарантирует «тихие» потери. Перед переключением (Caddy → Traefik, Nginx → Caddy, Apache → Nginx) выпишите короткий чеклист: какие заголовки, перенаправления, правила кеша сейчас отдаёт продакшен. После миграции — пройдитесь по чеклисту тем же набором команд. У нас отсутствие этого списка обошлось в две недели незамеченных регрессий. В деньгах — потенциально 1–3 месяца понижения позиций в Яндексе и Google.
Урок 2 — аудит после деплоя обязателен, даже если «ничего не должно было сломаться». Один автоматический аудит за 30 минут поймал три критические регрессии. Без него они тихо жили бы до следующей плановой проверки — это месяцы. На все наши проекты теперь после деплоя обязательный snapshot-аудит и сравнение с предыдущим.
Урок 3 — чеклист как часть инфраструктуры, не как разовое решение. Скрипт из 17 строк bash + curl запускается за 10 секунд после каждого деплоя. Не зависит от тестов в коде (тесты проходят на dev и могут падать на prod из-за хостинга), не требует тяжёлых инструментов. Запускается везде, где есть bash и curl. Когда задачи добавятся (новые страницы, новые типы микроразметки) — скрипт расширяется, не переписывается.
Технически: когда подход НЕ работает
Pattern «file middleware + docker labels» предполагает control над Traefik static config. Если хостинг не даёт доступ к /etc/dokploy/traefik/dynamic/ — file middleware недоступен.
На Coolify аналог через UI security-headers, но кастомные middleware (типа redirectRegex) ограничены. На Vercel / Netlify / Cloudflare Pages — headers через _headers file или vercel.json (другой синтаксис, но та же логика). На голом nginx / Apache shared hosting — через .htaccess (как на ozsm.ru) или nginx server-block.
Универсальная мысль: список middleware/headers/redirects должен быть declarative и жить в git, не в head-админа.
Single-sample audit недостаточен: PSI Mobile lab variance ±5-7 points между samples даже на стабильной странице. На ws DEV-сервере (десяток других контейнеров делят CPU/RAM/network) variance иногда доходит до ±10 points. Single sample может показать 90 (псевдо-регрессия) или 97 (псевдо-победа). Решение: 5 samples в один заход + median.
Что осталось на следующие фазы
Технический фронт закрыт. Дальше — три параллельных трека:
Phase 2 — контент для AI-поиска. Переписать первый абзац каждой страницы в формате прямого ответа (40-word answer rule — это то, что AI-движки цитируют). Добавить FAQ-блоки с типовыми вопросами по каждой услуге и кейсу. Этот трек идёт параллельным двух-недельным спринтом сразу после Phase 1.
Phase 3 — измерения и автоматика. Подключение Я.Вебмастера и Google Search Console к нашей внутренней аналитике. Авто-пинг IndexNow по различию sitemap. Еженедельный дайджест: «нас процитировали в ChatGPT / Perplexity / Я.Нейро N раз». Этот трек ждёт реальных данных от Google за первый месяц после изменений (CrUX field data доступна примерно с 1 июня).
Phase 4 — репутация и упоминания. Активные профили в VK, Telegram, Habr, Я.Бизнес, vc.ru, Tenchat. Возможность создания Wikipedia-сущности. Это не разработка — это 2–3 месяца community management.
Главное наблюдение: для B2B IT в 2026 году технический SEO — обязательный гигиенический минимум, а реальный рост даёт контент + авторитет + готовность сайта быть процитированным в AI-ответах. Phase 1 убрал технические причины «нас режут». Phase 2–4 строит причины «нас цитируют».
Связано
См. также статью «как мы собрали этот сайт», кейс Dokploy в продакшене с деталями миграции на ws DEV-сервер и кейс самого webshturm.ru с итоговыми метриками Phase 1.