Dual-mode, глоссарий и управление ценами из Supabase: три волны модернизации webshturm.ru
Услуги
- complex-systems
- seo-ai-search
- legacy-modernization
Стек
- Astro 5
- Svelte 5
- Tailwind 4
- Supabase Postgres 17
- PostgREST
- vitest
- rehype
- remark-directive
- gray-matter
Собственный сайт ООО «Веб Штурм» — IT-услуги B2B. Три волны модернизации webshturm.ru за июнь 2026: исправление светлой темы, запуск dual-mode «Разработчику/Руководителю» и управление ценами из Supabase без передеплоя. 285 автотестов, контраст WCAG AA 5.30
, правка калькуляторов из базы данных отражается на проде за ≤60 секунд.Контекст
Сайт webshturm.ru запущен в мае 2026 на Astro 5 + Svelte 5 + Tailwind 4 (кейс «Как мы делали свой сайт»). Поступили два отзыва от партнёров: светлая тема выглядит бледно, тексты слишком технические для руководителей. Параллельно потребовалось управлять коэффициентами калькуляторов без цикла сборки.
Реализовали три независимые задачи последовательно — каждая полностью протестирована перед началом следующей.
Волна 1 — светлая тема: корень бага в одной строке
Симптом: партнёры жаловались на «расползающуюся вёрстку» в светлом режиме. Аудит показал, что layout-дефектов нет: 0 визуальных расхождений на 59 страницах (desktop + mobile). Проблема — в контрасте текста.
Корень: class="prose prose-invert" хардкодом в ServiceLayout.astro и смежных layout’ах. Класс prose-invert переключает Tailwind Typography на светлую типографику — всегда, вне зависимости от текущей темы. На белом фоне получался почти незаметный серый текст.
Фикс: prose dark:prose-invert — prose-invert применяется только при тёмной теме. Дополнительно ввели токен --accent-strong (oklch(0.50 0.15 162) для light / oklch(0.80 0.15 162) для dark) вместо использования акцентного цвета как текстового: контраст вырос с 2.47
Guard-тесты проверяют, что text-accent (без strong) не попадает в типографику.
Волна 2 — dual-mode «Разработчику/Руководителю»
Архитектурное решение: параллельные коллекции
Сайт статически генерируется (Astro SSG, prerender=true). Для двух индексируемых аудиторий — два URL-дерева:
/— текущие URL, контент вsrc/content/{services,cases,blog}. SEO-equity Phase 1/2 сохранено./biznesu/— зеркало, контент вsrc/content/biz/{services,cases,blog}. Схемы вынесены в общие переменные,devиbizколлекции используют одну схему без дублирования.
AudienceToggle (Svelte 5 runes)
Тоглер в Navbar вычисляет зеркальный путь (toMirror), запоминает выбор в localStorage (ключ webshturm.audience.v1) и устанавливает cookie для серверной согласованности. Серверных редиректов нет — краулер видит именно запрошенный URL. Тоглер скрыт, если у текущей страницы нет biz-зеркала (итеративная выкатка контента).
Поймали open-redirect в toMirror через автоматический security-review. Фикс: нормализация ведущих слэшей + компонентный гард на протокол-relative URL.
rehype-глоссарий
rehype-glossary.mjs оборачивает первое вхождение термина из glossary.ts в <span class='gloss'> c тултипом. Кириллица потребовала Unicode lookarounds вместо стандартных \b (стандартные не обрабатывают символы за пределами ASCII). Тултип — position:absolute, без влияния на CLS.
Анти-каннибализм: машинный гейт
cannibalization.ts + vitest-тест читают оба дерева контента через gray-matter и проверяют: у dev↔biz пары с одним slug обязан различаться title (для cases), metaTitle/metaDescription/title (для services), title/excerpt (для blog). Тест падает при совпадении любого из полей — CI-деплой блокируется до правки контента.
Итог Волны 2
59 страниц собраны, 271/271 тестов зелёных после задачи. Пилотный manager-контент (3 страницы) прошёл ревью пользователя: тон, факты, анти-каннибализм по интенту подтверждены.
Волна 3 — цены и калькуляторы из Supabase
Задача
Калькуляторы (SEO-аудит, Pentest, Legacy-миграция) содержали коэффициенты в TypeScript-константах: изменить — только через коммит и деплой. Нужно: (1) редактировать коэффициенты из Supabase Studio без передеплоя, (2) скрывать/показывать блок цен без передеплоя, (3) цена должна быть в HTML (видна поисковым и AI-ботам — AEO).
Миграции и схема
Четыре миграции 0007–0010 добавили в схему webshturm:
calc_configs(service_id PK, config JSONB, is_active) — конфигурация калькуляторов.price_blocks(service_key PK, is_visible BOOLEAN, note) — флаги видимости блоков.*_versions+ BEFORE UPDATE триггеры — снапшот предыдущей версии при изменении контентных полей (история правок без отдельного audit log).
RLS включён, GRANT только для service_role. Anon и authenticated доступа не имеют.
Архитектура fail-open
Функции lib/calculators.ts принимают config аргументом (тестируемы без БД) и экспортируют DEFAULT_*_CONFIG — fallback при недоступной базе.
API-роуты /api/pricing/{calculators,visibility} (prerender=false): кэш 60 секунд на уровне Node-модуля, fail-open (ошибка БД → 200 + {} → компонент использует DEFAULT). Калькуляторы-острова делают fetch в onMount — HTML рендерится мгновенно с DEFAULT-значениями (без CLS), затем обновляются данными из БД.
Цена в HTML (AEO)
remark-price.mjs превращает директиву :::price{service=X} в <section data-price-block="X"> при сборке. Содержимое — статический HTML с ценой. PriceVisibility.svelte (client
is_visible===false. Краулер и AI-боты видят цену без JavaScript.
Инфра-гоча: Supabase cutover без грантов
Схему webshturm перенесли на новый хост дампом. PostgREST вернул 403 «permission denied for schema webshturm» — роль service_role не имела USAGE на схему. GRANT был выдан вручную. Урок: после переноса схемы — всегда проверять has_schema_privilege('service_role','<schema>','USAGE') и GRANT на таблицы.
Итог Волны 3
285/285 тестов зелёных. Runtime-e2e подтверждён на проде: UPDATE price_blocks SET is_visible=false в Supabase Studio → блок скрыт на webshturm.ru через 60 секунд без передеплоя. Revert → виден снова.
Что использовали
Astro 5 + Svelte 5 runes + Tailwind 4, Supabase Postgres 17 + PostgREST + RLS, vitest (285 тестов), rehype + remark-directive (глоссарий + директива цен), gray-matter (анти-каннибализм гейт), Gitea Actions (CI, авто-деплой).
Что можем сделать у вас
Если сайт существует в одном режиме, а клиенты — разные (технари и руководители), или нужно управлять ценами без привлечения разработчика — реализуем аналогичную архитектуру на вашем стеке. Обсудить →
Часто задаваемые вопросы
Почему dual-mode реализован через параллельные коллекции, а не client-side тоглом?
Как устроен глоссарий-тултип и почему кириллица требовала отдельного решения?
Как цена запекается в HTML и при этом остаётся скрываемой без пересборки?
Что такое open-redirect и как его поймали в AudienceToggle?
Что произошло с деплоем после добавления SSR-роутов?
Похожая задача?
Расскажите контекст — подскажем, что и как делать.