Архитектура

Alterna · онлайн-платформа турецкого A1–C1 по подписке

10 дней от $50 / мес
Документ v5

Веб-платформа турецкого по подписке: пять городов и 310 гайдов

План как в Duolingo, но материала больше, чем тестов. Каждый уровень — отдельный город. Каждая тема — гайд со своим дизайном. Дашборд показывает всё: от уровня ученика до секции, на которой он остановился. Только веб: телефон и компьютер в браузере.

310гайдов: 158 грамматика + 152 лексика
5городов, 960 часов, 5 000 слов
10дней до запуска MVP

Исходные требования

📚
310 гайдов с нуляграмматика и лексика, у каждого свой дизайн
🗺️
Уровень = городСтамбул → Измир → Анталья → Анкара → Самсун
💳
Подпискапробный, ученик, админ, суперадмин
📈
Подробная аналитикауровень, до какой секции дочитан гайд, время
👥
1 000–5 000 в деньдо 10 000 аккаунтов, дальше рост
1

Свобода внутри, контракт снаружи

У каждого гайда свой дизайн и свои анимации. Но любой гайд одинаково размечает секции и упражнения и подключает один трекер. Платформе неважно, как гайд выглядит.

2

Минимум сервисов

Vercel, Supabase и Paddle закрывают хостинг, базу, вход, файлы и подписки. Ни одного сервера, который нужно администрировать. За 10 дней это решает.

3

Трекинг — часть продукта

Прогресс чтения ученик видит в дашборде, поэтому он хранится в своей базе. PostHog нужен команде для решений: воронки, удержание, конверсия из пробного в платный.

4

Карты — источник истины

Города, юниты и порядок уроков строятся из карт грамматики и лексики скриптом. Поменял карту — план пересобрался, код трогать не нужно.

Что уже есть. Карты грамматики (158 тем) и лексики (152 темы) со стабильными id, часами, пререквизитами и привязкой к городам. Это готовый скелет базы данных. Гайды делаются с нуля под контракт платформы. Язык пока один — турецкий, но в схеме есть course_id, поэтому второй язык потом добавляется без переделки.

Маршрут городов

Каждый уровень — город

Экран «План» — это карта маршрута. Ученик проходит уровень в одном городе: модули привязаны к его фокусу, в конце — «Остановка» с темой о самом городе и тест уровня, который открывает следующий город.

1 / 5A1
Стамбул
фразы, кафе, транспорт
Гайды
73 · 45 + 28
Часы
120
Словарь
750 слов
2 / 5A2
Измир
покупки, жильё, город
Гайды
60 · 30 + 30
Часы
120
Словарь
1 500 слов
3 / 5B1
Анталья
университет, аренда, работа
Гайды
61 · 27 + 34
Часы
235
Словарь
2 500 слов
4 / 5B2
Анкара
документы, собеседования
Гайды
49 · 21 + 28
Часы
225
Словарь
3 700 слов
5 / 5C1
Самсун
культура, дискуссии, свобода речи
Гайды
67 · 35 + 32
Часы
260
Словарь
5 000 слов
Гайды: грамматика + лексикаВсего: 310 гайдов · 960 часов · 5 000 словданные из карт грамматики и лексики A1–C1

Как устроен город

Прибытие
Только в Измире и АнкареБытовые темы под фокус города. Идут раньше нужной грамматики, готовыми фразами, и ничего не блокируют.
Юниты 1…N
Модули из карт, 5–7 уроков в юнитеКак Unit 1 и Unit 2 на макете.
📘 Гайд грамматики A1-20 · дательный падеж 🍽️ Гайд лексики V-A1-18 · кафе ✍️ Практика упражнения 🎮 Игра слова юнита 🎯 Тест юнита
Остановка
Финал уровняТема о самом городе: места, еда, история.
Инструменты
Инструменты словаряФормулы общения, составные глаголы, словообразование, идиомы.
Тест уровня
Билет в следующий городОриентирован на экзамен TÖMER, как и порядок тем в карте.
Построитель плана: карты → города и юниты
// scripts/build-plan.ts — запускается в CI,
// когда меняется любая из карт
for (const city of route) {          // A1 Стамбул … C1 Самсун
  plan.push(arrival(city))             // только A2 и B2

  // грамматика — по order, граф пререквизитов
  const spine = topoSort(grammar[city.level])

  // лексика встаёт после своей грамматики
  for (const v of vocab[city.level])
    insertAfter(spine, v.recommended_after_grammar, v)

  // режем на юниты по 5–7 уроков
  plan.push(...chunk(spine, { min: 5, max: 7 })
    .map(withPractice).map(withUnitTest))

  plan.push(cityStop(city), tools(city), levelTest(city))
}
// результат → таблицы units и lessons в Postgres;
// админ может вручную подвинуть урок (override)

Правила открытия уроков

жёстко

Грамматика

Тема открыта, когда пройдены её пререквизиты из карты. CI проверяет, что в графе нет циклов.

мягко

Лексика

Рекомендована после своей грамматики (recommended_after_grammar), но не блокируется.

всегда

Прибытие и повторение

Открыты сразу. Прибытие даётся готовыми фразами, повторение слов доступно в любой момент.

по тесту

Следующий город

Открывается тестом уровня. Тест при регистрации может сразу отправить ученика в нужный город.

Прогресс в часах, а не в процентах. У каждой темы в карте есть часы, у уровня — модель: уроки, повторение, тесты. Поэтому дашборд может сказать «до Измира ≈ 37 часов — около трёх месяцев при 25 минутах в день». Темы, у которых ещё нет гайда, показываются как «скоро» и не мешают проходить город.

Стек технологий · проверено 9 октября 2026

Технологии: итоговое решение

Выбор под три условия: только веб, платная подписка, 10 дней на одного разработчика с Claude Code. Версии — текущие стабильные в npm на сегодня, тарифы и ограничения сверены с документацией сервисов (источники — внизу раздела).

Решение
Next.js 16 + Supabase + Paddle на Vercel
  • Один язык, TypeScript, — от базы до гайдов
  • Три основных сервиса вместо десяти: меньше настроек и точек отказа
  • Ни одного сервера, который нужно администрировать
  • План Б для закона РК: тот же код в Docker на сервере в Казахстане
СлойВыборВерсияПочему
ФреймворкNext.js, App Router16.4Серверные компоненты, Server Actions, кэш через "use cache", proxy.ts для проверки входа. Лучшая поддержка на Vercel.
UIReact + TypeScript19.3Стандарт рынка. Claude Code пишет его лучше всего.
Стили и компонентыTailwind CSS + shadcn/ui4.3 CLI 4.21Макет собирается из готовых компонентов, а их код лежит в твоём репозитории — меняешь как хочешь.
Анимации интерфейсаMotion14XP, стрики, переходы, карта городов.
ГрафикиRecharts3.10Дашборд ученика и аналитика в админке.
Формы и схемыzod + react-hook-form4.6Одна схема проверяет форму, API и контент гайдов.
База, вход, файлыSupabase: Postgres, Auth, Storage, pg_cronjs 2.117 ssr 0.12Всё бэкенд-ядро в одном сервисе, RLS из коробки. Переносится на свой сервер без переписывания кода.
Гайды и игрыHTML + CSS + GSAP + kitGSAP 3.15Полная свобода дизайна. GSAP со всеми плагинами бесплатен и в платных продуктах.
Повторение словts-fsrs5.4Алгоритм FSRS, как в Anki.
ОплатаPaddle Billingjs 1.6 node 3.10Merchant of Record: подписки, повторные списания, налоги за тебя. Продавцы из Казахстана поддерживаются.
ПисьмаResend + React Email6.32Письма с твоего домена, включая письма входа Supabase.
ОзвучкаAzure Speech, голоса tr-TR Emel и Ahmet—Нейроголоса, SSML для медленной речи, около $15 за 1 млн символов.
Ошибки и аналитикаSentry + PostHog11.6Ошибки, воронки, конверсия пробного в платный.
Тесты и линтерVitest, Playwright, Biome5.0 1.64Логика, сквозные сценарии на 375 px, один инструмент для линта и форматирования.
ХостингVercel Pro (fra1) + Supabase Pro (Франкфурт)—Рядом друг с другом; бесплатный Hobby на Vercel запрещает коммерческое использование.

Что изменилось в 2025–2026 и важно не пропустить

Next.js 16: proxy.ts

middleware.ts переименован в proxy.ts и по умолчанию работает на Node.js. Кэш включается явно: cacheComponents: true и "use cache".

Supabase: новые ключи

Ключи sb_publishable_… и sb_secret_… вместо anon и service_role — старые отключают в конце 2026. Подпись JWT асимметричная, проверка через getClaims().

GSAP теперь бесплатный

С апреля 2025 все плагины бесплатны и для коммерческих проектов. Запрещено только делать на нём конструктор анимаций, конкурирующий с Webflow.

Эти детали прописаны в CLAUDE.md, чтобы Claude Code не писал код по устаревшим туториалам.

Почему не другие варианты

Vue / Nuxt

Нормальный фреймворк, но меньше готовых компонентов, а ИИ-ассистенты пишут его хуже, чем React.

Firebase

NoSQL: связи «ученик — тема — секция» и аналитика в нём тяжелее. На свой сервер в РК не перенести.

Stripe

Не работает с продавцами из Казахстана.

Свой сервер, Docker, Kubernetes

Администрирование съест несколько дней из десяти. Нужно только для плана Б.

Отдельный бэкенд на NestJS или Go

Второй проект, второй деплой. Next.js и Postgres-функции закрывают всё, что нужно.

Местный эквайринг сразу

Freedom Pay или TipTop Pay дешевле по комиссии, но подписки придётся писать самому. Это после запуска.

Схема

Как всё связано

Ученик работает в приложении. Гайд открывается внутри приложения во весь экран, как урок в Duolingo. Сервер отдаёт гайд только тем, у кого есть доступ. События чтения идут через приложение на сервер и в Postgres.

Схема архитектуры Браузер ученика: приложение Next.js и гайд в iframe, связанные через postMessage. Приложение ходит на Vercel по HTTPS. Vercel проверяет доступ, читает гайд из приватного хранилища Supabase и пишет данные в Postgres. Kit и игры раздаёт CDN Vercel. Оплата — Paddle, вебхуки приходят на сервер. GitHub Actions публикует гайды и синхронизирует каталог. БРАУЗЕР УЧЕНИКА телефон или компьютер Приложение · Next.js Маршрут · Дашборд · Игры знает ученика и доступ копит события → батч postMessage Гайд или игра · iframe HTML + GSAP + kit tracker.js видит секции упражнения из kit игры — через game-sdk HTTPS Vercel · Next.js 16 /g/… — гайд после проверки /api/track — события /api/v1 — уроки, XP /api/webhooks — Paddle HTML гайда CDN Vercel /kit/v1 — kit.js, tracker.js GSAP, шрифты /games — код игр кэш навсегда статика ключевые события PostHog воронки, конверсия Sentry ошибки SQL+RLS Supabase · Франкфурт Postgres Auth Storage pg_cron каталог: 310 тем, план, города подписки, роли, журнал прогресс · события · FSRS гайды — приватный бакет аудио, картинки — публичный Resend письма Paddle подписки GitHub Actions · один репозиторий каждый мерж в main, публикуется только изменённое карты + guide.json → проверка схем → Playwright · 375 px → озвучка + Storage → content-sync деплой upload + sync

← Схему можно листать вбок →

Чтение. iframe запрашивает /g/a1.dative/v1 на том же домене. Сервер проверяет доступ и отдаёт HTML из приватного бакета. Без доступа — 403 и пейволл.

Трекинг. tracker.js сообщает приложению о секциях и ответах. Приложение добавляет пользователя и раз в 30 секунд отправляет пачку событий на /api/track.

Публикация. Новый гайд — мерж в репозиторий. CI проверяет, озвучивает, кладёт в Storage и синхронизирует каталог. Релиз приложения не нужен.

Фронтенд

HTML, React, Vue или что-то другое

Платформа и гайды решают разные задачи, поэтому одна технология на всё — ошибка. Платформа — веб-приложение с входом, подпиской, состоянием и данными. Гайд — длинная страница-история с тяжёлой анимацией, которую читают сверху вниз.

Платформа
React на Next.js 16
  • Быстрее всего с Claude Code. Нейросети лучше всего пишут React и Next.js — при сроке 10 дней это решает.
  • Всё для SaaS из коробки. Серверные компоненты, Server Actions, кэш каталога, proxy.ts для проверки входа.
  • Дизайн с макета — из готового. shadcn/ui, Motion, Recharts закрывают большую часть экранов.
  • Найм. React-разработчиков на рынке больше всего.
Гайды и игры
HTML + CSS + GSAP и общий kit
  • Без сборки. Гайд — один index.html. Claude Code генерирует такую страницу быстрее и надёжнее всего.
  • Общее не копируется. Упражнения, озвучка, карточки слов и трекер — web components из kit.js, подключаются одной строкой.
  • Один код упражнений. React 19 понимает web components, поэтому упражнение в гайде и в уроке «Практика» — одно и то же.
  • Быстро на слабом телефоне. Без фреймворка: только GSAP и код гайда.

Сравнение вариантов

ВариантДля платформыДля гайдов
React + Next.js✓Экосистема, скорость с ИИ, всё для SaaS.~Лишний рантайм, React и GSAP спорят за DOM.
HTML + GSAP + kit✕Вход, подписка, роутинг и состояние пришлось бы писать руками.✓Без сборки, полная свобода дизайна, общие части — из kit.
Vue + Nuxt~Хороший фреймворк, но меньше готовых компонентов и хуже результат у ИИ-ассистентов.~Те же минусы, что у React.
Astro✕Не для приложения с личными данными.~Хорош, но это ещё один шаг сборки. Можно перейти позже.
Чистый HTML без kit✕—✕Квиз, озвучка и трекер окажутся в 310 копиях.

Общие части

kit/kit.css

Цвета, шрифты, радиусы как CSS-переменные. Один источник для приложения, гайдов и игр.

приложениегайдыигры
kit/kit.js

Web components: <a-say> озвучка, <a-word> карточка, <a-exercise> упражнения из карты.

приложениегайды
kit/tracker.js

Без зависимостей: секции, активное время, ответы, озвучка → postMessage.

гайды
kit/game-sdk.js

Связь игры с платформой: получить слова, отправить очки, завершить игру.

игры
src/domain

Чистая логика: правила открытия, доступ, XP, часы до города, FSRS. С тестами Vitest.

приложениесервер
src/content/schema

Zod-схемы карт, guide.json, упражнений и game.json. Проверка в CI и типы в коде.

CIвсё

Экраны платформы

Лендинг и тарифыМаршрут (карта городов)Плеер гайдаПрактика и тестыДашбордПовторение словГрамматикаЛексикаРесурсыИгрыПодпискаНастройкиАдминкаСуперадмин
ЭкранКак рендерится
Лендинг, тарифыСтатика — SEO и мгновенная загрузка
Маршрут и юнитыServer Components — данные сразу, без спиннеров
Плеер гайда, игрыКлиент + iframe, трекер через postMessage
ДашбордServer Components из готовых агрегатов
Упражнения и тестыКлиент: проверка мгновенно, XP начисляет сервер
АдминкаServer Components + Server Actions, доступ по роли
ЗадачаБиблиотека
ИнтерфейсTailwind CSS 4 + shadcn/ui
Данные с сервераServer Components + Server Actions
Состояние плеераZustand
Формыreact-hook-form + zod
Карта городовSVG + Motion
Графики дашбордаRecharts
Повторение словts-fsrs
Устройства

От Android до ультраширокого монитора

Один код без отдельной мобильной версии. Верстаем сначала под самый узкий Android (360 px), потом добавляем место. Раскладку меняет ширина экрана, а не тип устройства: User-Agent не используем — iPad в Safari представляется как Mac.

до 767 px

Телефон

Android, iPhone

Одна колонка, нижняя панель из 5 вкладок, гайд во весь экран. Учитываем вырез и полоску «домой».

768–1023 px

Планшет вертикально

iPad, Android-планшеты

Панель иконок слева, карточки в две колонки.

1024–1599 px

Ноутбук и iPad горизонтально

MacBook, iPad landscape

Полный сайдбар, план в две колонки — как на макете.

1600–2559 px

Большой монитор

Full HD, 2K

Справа появляется колонка: цель дня, слова на повторение, стрик.

от 2560 px

Ультраширокий

3440×1440, 5K

Весь интерфейс по центру, не шире 2000 px. Текст не растягивается: строка до 75 знаков.

Правила вёрстки

  • Ширины — через max-width и clamp(), фиксированных ширин блоков нет.
  • Компоненты подстраиваются под свой контейнер (@container в Tailwind 4): карточка одинаково хорошо выглядит в сайдбаре и в центре.
  • Высота экрана — dvh вместо vh: адресная строка Safari и Chrome не обрезает контент.
  • Кнопки не меньше 44×44 px; hover — только для мыши: @media (hover: hover).
  • Шрифт в полях ввода не меньше 16 px — иначе iPhone приближает страницу.
  • Иконки — SVG, картинки — AVIF/WebP с версиями для Retina.

Гайд на разных экранах

  • Телефон: во весь экран, верхняя панель 48 px, кнопка «назад» Android закрывает гайд, а не уходит с платформы.
  • Ноутбук и iPad горизонтально: гайд не шире 1100 px по центру.
  • От 1600 px: слева оглавление секций с галочками, справа слова гайда.
  • Медиазапросы гайда смотрят на ширину iframe, поэтому гайд тоже верстается mobile-first.
  • При повороте экрана анимации GSAP пересчитываются; при prefers-reduced-motion показывается итог без анимации.
  • content:check снимает каждый гайд на 360, 768, 1280 и 1920 px.

Особенности платформ

iPhone · Safari

viewport-fit=cover и env(safe-area-inset-*) для выреза и полоски «домой». Звук — только по нажатию: автозапуск заблокирован.

Android · Chrome, Samsung

Кнопка «назад» закрывает плеер через историю браузера. Проверка на слабом телефоне с замедлением процессора ×4.

iPad

Safari выдаёт себя за Mac, поэтому различаем по (pointer: coarse). С клавиатурой и трекпадом работают hover и горячие клавиши. Поворот меняет раскладку без перезагрузки.

MacBook

Чёткие SVG и картинки 2x для Retina, hover-состояния, горячие клавиши в упражнениях: 1–4 — выбор, Enter — проверить.

Ультраширокий монитор

Контент и графики не растягиваются на всю ширину. Таблицы админки получают больше колонок, а не шире ячейки.

Как проверяем

Профиль PlaywrightДвижокРазмерЧто это
androidChromium, касания360×800Бюджетный Android
iphone-seWebKit, касания375×667Маленький iPhone
iphoneWebKit, касания393×852iPhone 15–17
iphone-maxWebKit, касания430×932Pro Max
ipadWebKit, касания820×1180 и 1180×820iPad в двух ориентациях
macbookChromium, Retina ×21470×956MacBook Air 13
fhdChromium1920×1080Обычный монитор
ultrawideChromium3440×1440Ультраширокий

На каждом профиле

  • нет горизонтального скролла
  • ничего не уходит под нижнюю панель и вырез
  • главные кнопки видны и нажимаются
  • скриншоты «Плана», плеера, дашборда и тарифов совпадают с эталоном
  • Lighthouse mobile на «Плане» и в плеере — не ниже 85

Поддерживаемые браузеры. Safari 16.4+ (iPhone 8 и новее), Chrome 111+, Firefox 128+, Samsung Internet, Edge — это требование Tailwind CSS 4. На iPhone 7 и старше (iOS 15) показываем плашку «Обновите браузер». Перед запуском главный сценарий проходится руками на дешёвом Android, iPhone, iPad и MacBook.

Гайды

310 уникальных страниц и один контракт

Гайды делаются с нуля под платформу, поэтому контракт закладывается с первого гайда: внутри — любой дизайн, снаружи — обязательные атрибуты. Id гайда = id темы из карты. Всё живёт в одном репозитории с платформой.

Структура репозитория
alterna/
  src/                  # платформа, Next.js 16
    app/  domain/  content/schema/
  public/
    kit/v1/             # kit.css, kit.js, tracker.js,
                        # game-sdk.js, GSAP
    games/match-pairs/  # index.html, game.json
  guides/
    _template/          # шаблон для Claude
    a1-istanbul/
      a1.dative/        # index.html, guide.json,
                        # exercises.json, media/
      v.a1.cafe/
    a2-izmir/ …         # 310 папок
  content/
    maps/               # карты грамматики и лексики
  supabase/migrations/
  scripts/              # build-plan, publish, sync
  CLAUDE.md
Разметка внутри гайда
<!-- дизайн любой, атрибуты обязательны -->
<section data-section="buffer-y"
         data-words="180">
  …свои анимации и вёрстка…
  <a-say>Arabaya bin.</a-say>
</section>

<!-- упражнение из общего движка -->
<a-exercise ref="a1.dative.ex2"></a-exercise>

<link rel="stylesheet" href="/kit/v1/kit.css">
<script type="module" src="/kit/v1/kit.js"></script>
<script src="/kit/v1/tracker.js" defer></script>

Озвучка генерируется при публикации: скрипт находит все <a-say>, создаёт аудио через Azure Speech и кладёт в публичный бакет.

guide.json
{
  "topic": "a1.dative",     // id из карты
  "code": "A1-20",
  "city": "istanbul",  "cefr": "A1",
  "kind": "grammar",         // или "lexicon"
  "version": 1,
  "title": "Дательный падеж -(y)A",
  "sections": [
    { "key": "idea",     "title": "Куда? Кому?",  "words": 160 },
    { "key": "harmony",  "title": "-a или -e",    "words": 240 },
    { "key": "buffer-y", "title": "arabaya",      "words": 180 },
    { "key": "pronouns", "title": "bana, sana",   "words": 120 },
    { "key": "quiz",     "title": "Тест", "type": "quiz" }
  ],
  "exercises": ["a1.dative.ex1", "a1.dative.ex2"],
  "vocab": ["gitmek", "vermek", "ev", "okul"]
}

CI не пропустит гайд, если

  • topic не найден в карте или уровень гайда не совпадает с уровнем темы;
  • секция в разметке не совпадает с guide.json или ключ повторяется;
  • на 375 px есть горизонтальный скролл или ошибки в консоли (Playwright);
  • JS + CSS гайда тяжелее бюджета — например, 400 КБ без медиа;
  • у упражнения нет темы и навыка — тогда дашборд не поймёт, что у ученика слабое.

Ключи секций — навсегда. Прогресс хранится по ключу, а не по номеру. Если потом вставить секцию в середину, у тех, кто уже читал гайд, прогресс не съедет.

Доступ к гайдам по подписке

Ученик открывает урок, iframe запрашивает /g/a1.dative/v1
Запрос идёт на тот же домен — cookie сессии уходит сама
Сервер проверяет доступ: пробный период или подписка
Читает HTML из приватного бакета Supabase Storage
Отдаёт гайд: кэш в браузере, встраивание только в платформу

Без доступа сервер отвечает 403, приложение показывает пейволл. Аудио и картинки лежат в публичном бакете с CDN: без HTML гайда они бесполезны, зато кэшируются. Когда трафик медиа превысит 250 ГБ в месяц, его можно перенести в Cloudflare R2 — меняется одна переменная.

Игры

Блок игр: добавлять новые без переделки платформы

Игра устроена как гайд: папка со статикой, свой game.json и общий SDK. Сама игра ничего не знает о турецком — слова и фразы ей передаёт платформа из тем, которые ученик уже прошёл. Одна механика сразу работает для всех пяти городов.

game.json
{
  "id": "match-pairs",
  "title": "Найди пару",
  "skill": "lexicon",
  "content": "words",     // words | phrases | affixes
  "minLevel": "A1",
  "version": 1,
  "xpPerGame": 10,
  "dailyXpCap": 50
}
Внутри игры
const game = await AlternaGame.init()
// game.content.words →
// [{ tr: "elma", ru: "яблоко", audio: "…" }, …]

// … игра идёт …
game.score(120)
game.complete({ correct: 18, total: 20 })
// платформа сохраняет game_session,
// сервер начисляет XP с дневным лимитом

Как добавить игру

Папка public/games/<id>/ + game.json → мерж → Vercel выкладывает, CI регистрирует игру в каталоге. Через пару минут она в разделе «Игры». Позже — загрузка zip в суперадминке.

Безопасность

Игра запускается в iframe с sandbox="allow-scripts": у неё «чужое» происхождение, поэтому даже сторонний код не прочитает cookie и не сходит в API от имени ученика. Очки проверяет сервер, XP ограничен в день.

Что можно копировать

Механики («найди пару», wordle, виселица) не защищены — их можно брать. Чужой код, графику и названия — нельзя. Open-source игры с лицензией MIT или Apache можно адаптировать, сохранив лицензию.

Идеи первых игр

Найди пару: слово — переводСобери слово из буквАффикс на скорость: -da / -de / -ta / -teТурецкий wordleАудиодиктантСобери фразу из словВиселица

В 10 дней — SDK и одна игра («Найди пару»). Остальные добавляются папками после запуска.

Бэкенд и роли

Где лежат материалы и как текут данные

Главное правило: Git — источник истины для контента, Postgres — для учеников. Каталог в базе — только копия, которую перезаписывает CI. Поэтому любую правку контента можно проверить, откатить и найти в истории.

Где лежат материалы

ЧтоГдеПочему
Карты, гайды, упражнения, игрыGit, один репозиторийРевью через PR, история, откат одной командой.
Опубликованные гайдыSupabase Storage, приватный бакет: guides/a1.dative/v1/Отдаются только через сервер после проверки доступа. Каждая версия — своя папка, откат = вернуть номер версии.
Аудио и картинкиSupabase Storage, публичный бакет + CDNИмя файла = хеш содержимого: без дублей, кэш навсегда. При росте — Cloudflare R2 без платы за трафик.
Kit, GSAP, шрифты, игрыVercel CDN, папка public/Деплоятся вместе с приложением, версия в пути: /kit/v1/.
Каталог: темы, план, секции, словаPostgres (копия из Git)Быстрые запросы и связи с прогрессом учеников.
Ученики, подписки, прогрессPostgresТранзакции, RLS, ежедневные бэкапы.

Путь гайда от темы до ученика

  1. Тема из картыСтатус planned → ветка guide/a1.dative.
  2. Гайд пишетсяClaude Code по шаблону → index.html, guide.json, упражнения.
  3. Pull request → CIСхемы, контракт, Playwright на 375 px, бюджет веса.
  4. ПревьюПроверяешь гайд по ссылке из PR.
  5. Мерж → publishОзвучка, загрузка в guides/a1.dative/v1/.
  6. content-syncКаталог → Postgres, статус published, сброс кэша. Гайд появился у учеников.

Шесть потоков данных

1 · СтатикаKit, игры, аудио, картинки — большая часть трафика. CDN, сервер не участвует.
2 · ГайдыHTML через сервер после проверки доступа, дальше — кэш в браузере.
3 · КаталогПлан и темы одинаковы для всех. Кэш Next.js, сброс при публикации.
4 · ЛичноеПрогресс, дашборд, слова. Postgres по индексам, дашборд из агрегатов.
5 · События~17 запросов/с в пик. Батчи, пакетная вставка, без дублей.
6 · Транзакции и фонУрок пройден, XP, вебхуки Paddle, агрегаты, письма. Решает сервер.

Четыре роли

Пробный

Зарегистрировался, не платит. Доступ по правилам пробного периода.

После оплаты становится учеником автоматически, по вебхуку Paddle.

Ученик

Активная подписка: все города, гайды, упражнения, игры, дашборд.

Если оплата не прошла — Paddle повторяет списания, а доступ держится до конца периода.

Админ

Поддержка и контент: ученики, подписки, продление пробного, статусы тем, аналитика.

Не может назначать роли и менять цены.

Суперадмин

Ты, разработчик: всё, что может админ, плюс роли, цены, настройки, фича-флаги.

Назначается только SQL-миграцией, не из интерфейса.
Что можноПробныйУченикАдминСуперадмин
Маршрут, первые уроки Стамбула, одна игра✓✓✓✓
Все города, упражнения, тесты, повторение, все игры~по настройке✓✓✓
Свой дашборд и статистика✓✓✓✓
Ученики, подписки, продление пробного, поддержка✕✕✓✓
Статусы тем, аналитика гайдов и игр✕✕✓✓
Назначать админов, цены, пробный период, фича-флаги✕✕✕✓
«Войти как ученик» (только чтение, с журналом)✕✕✕✓
Удалять аккаунты и данные✕✕✕✓

Как это устроено

Роль
app_role в профиле: user, admin, superadmin. Попадает в JWT через Custom Access Token Hook, RLS читает её оттуда.
Доступ
«Пробный» и «ученик» — не роль, а статус: trial, student, expired. Считается из trial_ends_at и подписки. Если бы это была роль, её пришлось бы переключать руками — и кто-то бы забыл.
Запись
Прогресс, XP и доступ меняет только сервер: /api/v1 и Postgres-функции. «Урок пройден → XP → стрик → следующий урок» — одна транзакция.
Защита
Для админа и суперадмина обязательна двухфакторная защита (TOTP в Supabase Auth). Каждое их действие пишется в audit_log.
RLS: ученик видит себя, админы — всех
alter table user_guide_progress enable row level security;

create policy "свой прогресс"
  on user_guide_progress for select
  using (user_id = (select auth.uid()));

create policy "админы видят всех"
  on user_guide_progress for select
  using (((select auth.jwt()) ->> 'app_role')
         in ('admin', 'superadmin'));

-- политик insert/update нет:
-- писать может только сервер (секретный ключ)

-- суперадмин — только миграцией:
update profiles set app_role = 'superadmin'
 where id = '<твой user id>';

Безопасность

  • RLS на каждой таблице; секретный ключ Supabase только на сервере и в CI
  • postMessage: проверка источника с обеих сторон
  • Гайды встраиваются только в платформу (frame-ancestors 'self')
  • Лимит запросов на /api/track и вход; подпись вебхуков Paddle проверяется
  • XP и прохождение считает сервер, клиент сообщает только факты
  • Ежедневные бэкапы; минимум личных данных; удаление аккаунта одной кнопкой
Хостинг и CI/CD

Где разместить платную платформу

Для подписочного продукта важны три вещи: тариф хостинга, который разрешает коммерческое использование, ежедневные бэкапы и платёжный провайдер, который работает с продавцами из твоей страны. Всё ниже — управляемые сервисы: серверы администрировать не нужно.

КомпонентГдеТарифПочему
ПлатформаVercel, регион fra1Pro, $20Бесплатный Hobby запрещает коммерческое использование. В Pro есть $20 кредита на использование, превью на каждый PR.
База, вход, файлыSupabase, ФранкфуртPro, от $25Бесплатный тариф засыпает без активности и без бэкапов. В Pro — ежедневные бэкапы и 250 ГБ трафика.
ОплатаPaddle Billing5% + $0,50 с платежаПодписки, повторные списания, налоги и счета. Выплаты раз в месяц в USD или EUR.
ПочтаResend$0–20Письма с твоего домена: вход, пробный заканчивается, оплата.
Домен и DNSCloudflareDNS бесплатно, домен ~$15–40 в годПозже здесь же — R2 для медиа, если вырастет трафик.

Vercel и Supabase — в одном регионе. Функции Vercel ставятся в fra1, рядом с базой во Франкфурте. Если оставить регион по умолчанию (США), каждый запрос к базе станет медленнее на 100+ мс.

Нужны ли CI/CD и Docker

CI/CD
Да, но минимальный
  • Vercel сам деплоит: каждый push → превью-ссылка, main → прод. Настраивать нечего.
  • GitHub Actions, один файл. На PR — типы, линтер, Vitest, Playwright по превью. На main — миграции, публикация изменённых гайдов, content-sync.
  • Миграции БД — только из Git. Схему на проде руками не трогаем никогда.
  • Три окружения: локально, превью (отдельный проект Supabase), прод.
Docker
Нет — для деплоя не нужен
  • Деплой: Vercel и Supabase сами управляют серверами. Dockerfile писать не нужно.
  • Локально: Docker Desktop или OrbStack нужен только как движок для supabase start.
  • План Б: если юрист скажет хранить данные в Казахстане — VPS в РК (Yandex Cloud kz1, PS Cloud) и Docker Compose: Next.js + self-hosted Supabase. Это +3–5 дней работы.
  • Kubernetes: нет. При такой нагрузке это лишняя сложность.
.github/workflows/ci.yml — суть
on: [pull_request, push]
jobs:
  check:          # каждый PR
    - pnpm typecheck && pnpm lint
    - pnpm test               # Vitest: domain, RLS
    - pnpm guide:check --changed
    - pnpm e2e --base-url=$PREVIEW_URL
  release:        # только main
    - supabase db push        # миграции
    - pnpm content:publish --changed
                              # озвучка, гайды → Storage
    - pnpm content:sync       # каталог → Postgres

Мониторинг

  • Sentry: ошибки клиента и сервера, алерт в Telegram
  • Uptime-монитор на главную и /api/health — проверка раз в минуту
  • Логи Vercel и Supabase; отдельный алерт на ошибки /api/track и вебхуков Paddle
  • Секреты — в Vercel и GitHub Secrets; секретный ключ Supabase никогда не попадает в браузер
Трекинг

Как узнать, до какой части дочитан гайд

Гайд — отдельная страница в iframe, он не знает ни про ученика, ни про базу. Он только сообщает факты через postMessage, а приложение дописывает пользователя и отправляет данные на сервер. Поэтому 310 гайдов не зависят от кода платформы.

Секция в фокусеIntersectionObserver замечает секцию в центре экрана
Активное времятолько когда вкладка открыта и ученик что-то делал последние 30 с
postMessagetracker.js передаёт событие приложению
Батчраз в 30 с и при закрытии — через sendBeacon
/api/trackпроверка сессии, запись в events, обновление прогресса
Дашбордночью pg_cron собирает агрегаты для графиков

Когда секция считается прочитанной

Если засчитывать секцию при простом показе, быстрый скролл до конца даст «100% прочитано». Поэтому учитываем время:

секция в фокусеи активное время ≥ 30% расчётногогде расчётное = data-words ÷ 150 слов в минуту

Гайд пройден, когда прочитано не меньше 80% секций и сдан тест в конце. Кнопка «Продолжить» на маршруте ведёт ровно в последнюю секцию.

СобытиеКуда пишем
guide_openevents, прогресс: начат
section_readevents, прогресс: секция и %
heartbeatтолько счётчик активного времени, не в events
exercise_answerexercise_attempts + events
audio_playevents
guide_completeпрогресс, XP, открытие следующего урока
Прогресс по гайду
create table user_guide_progress (
  user_id        uuid references auth.users,
  topic_id       text references topics,  -- a1.dative
  guide_version  int,
  last_section   text,     -- для «Продолжить»
  sections_read  text[],   -- ключи секций
  read_pct       smallint,
  active_seconds int,
  quiz_best_pct  smallint,
  started_at     timestamptz,
  completed_at   timestamptz,
  primary key (user_id, topic_id)
);

Слой 1 — своя база

Всё, что видит ученик: секции, время, ответы, стрики. Сырые события лежат в таблице с партициями по месяцам и живут 90 дней, агрегаты — всегда.

Слой 2 — PostHog

Только ключевые события: регистрация, первый гайд, пробный → оплата, город пройден, отмена подписки. Детальные события туда не шлём: при 5 000 учеников в день это десятки миллионов событий и сотни долларов в месяц.

Данные и дашборд

Что хранится и что из этого видно

Таблицы каталога заполняет CI из карт, гайдов и игр. Таблицы учеников пишет только сервер. Админка и суперадминка — свои экраны в приложении.

Ученик и доступ

  • profiles
    имя, часовой пояс, роль, конец пробного периода
  • subscriptions, payments
    копия статуса из Paddle, история оплат
  • settings
    длина пробного, что в нём доступно, id цен Paddle

Маршрут и контент

  • cities → units → lessons
    строит build-plan.ts
  • topics, topic_prereqs, topic_links
    310 тем из карт, пререквизиты, связи
  • guides, guide_sections, exercises, vocab_items

Прогресс

  • user_lesson_progress
    закрыт, открыт, в процессе, пройден
  • user_guide_progress
    секции, %, время, лучший тест
  • exercise_attempts
    каждый ответ с темой и навыком

Слова

  • vocab_items
    слово, перевод, аудио, тема-источник
  • user_cards
    состояние FSRS: stability, difficulty, due

Игры и мотивация

  • games, game_sessions
    каталог игр, очки, время
  • xp_events, streaks
    за что XP, текущий и лучший стрик

Аналитика и аудит

  • events
    сырые, партиции по месяцам, 90 дней
  • daily_user_stats, guide_section_stats
    агрегаты и drop-off по секциям
  • audit_log
    все действия админов

Дашборд ученика

  • текущий город и часы до следующего
  • CEFR-уровень и уровень по каждому навыку
  • словарь: сколько слов из цели города уже выучено
  • каждый гайд: % прочитанного, где остановился, время, тест
  • минуты по дням, стрик, XP
  • слабые темы — по ошибкам в упражнениях

Аналитика в админке

  • drop-off по секциям каждого гайда: где закрывают
  • фактическое время на тему против часов из карты
  • самые трудные упражнения и вопросы
  • воронка: регистрация → первый гайд → оплата → Измир
  • конверсия пробного в платный, отток, MRR
  • карточка ученика: вся история по шагам

Как определяется уровень. При регистрации — адаптивный тест на 12–15 вопросов, он выбирает стартовый город. Дальше уровень растёт по пройденным городам, а уровни навыков считаются по ответам: у каждого упражнения есть навык и тема.

Нагрузка

Хватит ли мощности

Kit, игры и медиа раздаёт CDN, гайды кэшируются в браузере, каталог — в кэше Next.js. До базы доходит почти только личное: прогресс и события. Для Postgres это мало.

~500учеников онлайн в пик — около 10% от 5 000 в день
~17/сзапросов трекинга в пик при батче раз в 30 с
~5 млнсобытий в месяц, около 35 на ученика в день
×10запас: стек тянет 50 000 в день без переделки

Три слоя кэша

~95%
CDN и браузерKit, игры, аудио, картинки — кэш навсегда. HTML гайда — в браузере на час.
~4%
Кэш Next.jsМаршрут, темы, метаданные. Сбрасывается при публикации.
~1%
PostgresТолько личное: прогресс, события, подписка, слова на сегодня.

Доли — оценка по трафику. Пики: вечер 19:00–22:00, рассылки, начало учебного года. Соединения идут через пулер Supavisor, чтобы serverless-функции не исчерпали лимит базы.

Как расти дальше

до 10 000

Стек как есть: Supabase Pro с compute Small.

до 50 000

Compute Medium, реплика для чтения под дашборды, медиа — в Cloudflare R2.

100 000+

Сырые события → ClickHouse, очередь для событий и писем. Остальное не меняется.

Стоимость

Сколько всё стоит

Серверы — самая маленькая статья. Главные расходы: комиссия за оплату и время на контент. Цифры ориентировочные, по публичным тарифам на октябрь 2026 — проверь перед покупкой.

В месяцСтарт, до 1 0005 000 в день50 000 в день
Vercel Pro$20$20–40$60–150
Supabase Pro + compute$25$30–60$110–250
Resend$0–20$20$35–90
Sentry, PostHog$0$0–26$26–150
Cloudflare: DNS, позже R2$0$0$5–20
Домен~$2~$2~$2
Итого инфраструктура≈ $50–70≈ $75–150≈ $240–660

Разово и на время разработки

Claude Code (подписка Max) на месяц≈ $100–200
Озвучка 310 гайдов и 5 000 слов≈ $15–150
Домен на год$15–40
Юрист: оферта, политика, возвратыпо договорённости
Иллюстрации 5 городов$0 (генерация) – $500+
Без юриста≈ $130–900

Озвучка: около 1–1,5 млн символов. Azure или Google — $15–30 после бесплатного лимита, ElevenLabs — до $150.

С каждой оплаты

Paddle (налоги, счета, повторные списания)5% + $0,50
Подписка $8 в месяц → комиссия$0,90 · 11%
Подписка $60 в год → комиссия$3,50 · 6%
Местный эквайринг (после запуска)обычно 2,5–3,5%

Продвигай годовой тариф. Фиксированные $0,50 съедают дешёвые месячные подписки. При 1 000 подписчиков сервер обходится примерно в $0,07 на ученика в месяц, а комиссия — в $0,90 с месячного платежа.

План на 10 дней

MVP за 10 дней с Claude Code

10 дней — реально для рабочей платформы, если работать полный день и не расширять объём. Это не 310 гайдов: конвейер гайдов стартует на 3-й день и идёт параллельно. Подробные промпты на каждый день — в плане для Claude Code.

Сделай сегодня, до дня 1. Подай заявку в Paddle: проверка домена и личности занимает несколько дней. Для проверки нужен живой сайт на твоём домене с ценами, офертой, политикой конфиденциальности и правилами возврата — поэтому лендинг с этими страницами выходит в первый же день.

Честно про контент. 310 уникальных гайдов с проверкой — это 10–15 в день, то есть ещё 3–4 недели после старта конвейера. К запуску реально: первые юниты Стамбула. Остальные темы видны на маршруте как «скоро» и открываются по мере публикации.

ДЕНЬ1

Фундамент и лендинг

Opus · планSonnet · код
  • Next.js 16 + Tailwind 4 + shadcn/ui, Supabase локально и в облаке
  • Вход: email и Google; профили, роли в JWT, RLS
  • Каркас по макету в 5 раскладках — от телефона до ультраширокого; лендинг с ценами, офертой, политикой, возвратами
  • Деплой на Vercel (fra1), Sentry, PostHog
Готово, когда: лендинг и вход работают на твоём домене, /admin закрыт для обычного пользователя.
ДЕНЬ2

Карты → маршрут

Opus · схема БДSonnet · экран
  • Zod-схемы карт, таблицы тем, городов, юнитов, уроков
  • build-plan.ts и content-sync.ts, тесты графа
  • Экран «План»: карта 5 городов, юниты, статусы уроков
Готово, когда: план Стамбула строится из карт и виден в приложении.
ДЕНЬ3

Гайд, kit и доступ

Opus · доступSonnet · kit
  • kit.js, kit.css, tracker.js, шаблон гайда
  • content:publish: проверка контракта, озвучка, загрузка в Storage
  • Маршрут /g/…: отдаёт гайд из приватного бакета только при доступе
  • Старт конвейера гайдов
Готово, когда: тестовый гайд открывается только после входа, без входа — 403.
ДЕНЬ4

Плеер и трекинг

SonnetOpus · ревью
  • Плеер: во весь экран на телефоне, с оглавлением на широком экране; «Продолжить»
  • /api/track: батчи, без дублей, партиции
  • Урок пройден → XP → стрик → следующий урок, одной транзакцией
Готово, когда: прочитал гайд — в базе секции и время, следующий урок открыт.
ДЕНЬ5

Упражнения и тесты

Sonnet
  • <a-exercise>: 5 типов — пропуски, сопоставление, выбор, сборка фразы, верно/неверно
  • Уроки «Практика» и «Тест юнита»
  • Сервер проверяет ответ и начисляет XP
Готово, когда: первый юнит Стамбула проходится целиком.
ДЕНЬ6

Подписка и пробный период

Opus
  • Тарифы, Paddle Checkout в sandbox, вебхуки → подписки
  • Статус доступа: пробный, ученик, истёк; пейволл
  • Письма: добро пожаловать, пробный заканчивается, оплата прошла и не прошла
Готово, когда: тестовая оплата превращает пробного в ученика и открывает Измир.
ДЕНЬ7

Дашборд и повторение

Sonnet
  • Ночные агрегаты на pg_cron
  • Дашборд: город, часы до следующего, навыки, гайды, минуты
  • Повторение слов на FSRS с озвучкой; стрики по часовому поясу
Готово, когда: дашборд показывает реальные данные тестового ученика.
ДЕНЬ8

Админка, суперадмин, игры

Opus · праваSonnet · экраны
  • Админка: ученики, подписки, продление пробного, статусы тем, drop-off
  • Суперадмин: роли, настройки, фича-флаги, журнал
  • Двухфакторный вход для админов
  • game-sdk.js, раздел «Игры», первая игра «Найди пару»
Готово, когда: админ продлевает пробный период, а действие видно в журнале.
ДЕНЬ9

Качество и безопасность

Opus · max
  • Playwright на 9 профилях: Android, iPhone, iPad, MacBook, 1920, 3440 — регистрация → пробный → гайд → оплата → Измир
  • Тесты RLS: чужое не видно, пробный не откроет закрытый гайд
  • k6: 1 000 учеников шлют события, 95% ответов быстрее 300 мс
  • /code-review и /security-review по всему проекту
Готово, когда: все тесты зелёные, критичных замечаний ревью нет.
ДЕНЬ10

Запуск

Sonnet
  • Paddle в боевом режиме (после одобрения), прод-переменные
  • Юридические тексты от юриста на месте черновиков
  • Чек-лист запуска, бэкапы, алерты
  • Мягкий запуск на первых учеников
Готово, когда: первый настоящий ученик оплатил подписку.
∥ПараллельноС дня 3

Конвейер 310 гайдов

Город за городом, в порядке маршрута.

Стамбул 73Измир 60Анталья 61Анкара 49Самсун 67
  • Один промпт на гайд, шаблон и контракт уже в репозитории
  • Claude генерирует, ты проверяешь превью, CI проверяет контракт
  • 10–15 гайдов в день → все 310 примерно за 3–4 недели
✕Не в 10 днейПосле запуска

Что сознательно откладываем

Всё это не мешает запуску и лучше делается на реальных данных.

  • Местный эквайринг (Freedom Pay или TipTop Pay) для оплаты в тенге
  • Оценка произношения и свободное письмо с проверкой ИИ
  • Остальные 7 типов упражнений и новые игры
  • Вход через Telegram, другие языки, ClickHouse
Claude Code

Как вести разработку с Claude Code

Два файла делают Claude Code предсказуемым: CLAUDE.md — правила проекта, которые он читает в каждой сессии, и PLAN.md — промпты на каждый из 10 дней. Положи оба в корень нового репозитория.

Какую модель и effort выбирать

ЗадачаМодельEffortПочему
План дня (plan mode)Opus 5.5highОшибка в плане стоит дня работы. Opus лучше видит связи между частями.
Схема БД, RLS, роли, оплата, доступ к гайдамOpus 5.5highЗдесь деньги и безопасность — дешёвая модель тут обходится дороже.
Ревью безопасности (день 9), сложные багиOpus 5.5xhigh или maxРедко, но глубоко. Долго и дорого — только по делу.
Экраны, компоненты, API по готовому плануSonnet 5.5mediumОсновная масса кода. Быстро и экономно.
Трекер, построитель плана, движок упражненийSonnet 5.5highЛогика с краевыми случаями, но задача хорошо описана.
Гайды, упражнения, игры по шаблонуSonnet 5.5mediumМного однотипной работы. Opus — только для эталонного первого гайда.
Тексты, мелкие правки стилей, переименованияSonnet 5.5lowДумать не над чем.

Ритм одного дня

  1. Утро: план на OpusPlan mode (Shift+Tab), промпт дня из PLAN.md. Читаешь план, правишь, утверждаешь.
  2. День: код на Sonnet/model → Sonnet. Одна задача — одна сессия; /clear между задачами, чтобы контекст не засорялся.
  3. После каждой фичиТесты, проверка на 375 px, коммит. Превью-деплой на Vercel.
  4. Вечер: ревью на Opus/code-review по изменениям дня, правки, мерж в main.
  5. ПараллельноВо второй сессии Sonnet генерирует гайды по одному промпту на тему.
Решить до старта

Вопросы, которые меняют стек

Их нужно закрыть до дня 1 или в первые дни: от ответов зависит, где хранить данные, как принимать оплату и что видит пробный пользователь.

Где хранить данные учеников

Закон РК требует, чтобы база с персональными данными граждан находилась в Казахстане. Юристы спорят, допустима ли копия за рубежом, но основная база в РК устраивает оба толкования. С поправками 2026 года место хранения базы ещё и нужно указывать в уведомлении регулятору.

Решение для 10 дней: Supabase во Франкфурте, минимум личных данных (только email и имя) и консультация юриста сразу. Если нужна локализация — план Б: Docker Compose (Next.js + self-hosted Supabase) на сервере в РК, например Yandex Cloud kz1 или PS Cloud. Код не меняется, это +3–5 дней.

Как принимать оплату

Stripe не работает с продавцами из Казахстана. Paddle работает: Казахстана нет в списке запрещённых стран, НДС РК он считает и платит сам, выплаты раз в месяц в USD или EUR.

Решение: Paddle на старте, Polar — если Paddle откажет (Казахстан есть в списке выплат Polar). Местный эквайринг (Freedom Pay, TipTop Pay) — после запуска, когда выручка оправдает работу над своими подписками.

Как устроен пробный период

Варианты: 7 дней полного доступа без карты или первые уроки Стамбула навсегда.

Рекомендация: 7 дней полного доступа без карты (считает платформа, а не Paddle), после — остаются первые уроки Стамбула и пейволл. Всё хранится в settings, суперадмин меняет без релиза.

Цены и тарифы

Месяц и год, валюта, цена с НДС или без.

Из-за фиксированной комиссии $0,50 выгоднее продвигать годовой тариф. Цены живут в Paddle, в базе — только их id.

Способы входа

Email (код или ссылка) и Google — с первого дня.

Вход через Telegram популярен в СНГ, но в Supabase его нет из коробки. После запуска.

Бюджет скорости гайда

310 уникальных дизайнов без ограничений превратятся в 310 тяжёлых страниц на слабых телефонах.

Лимит 400 КБ JS + CSS без медиа и LCP меньше 2,5 с — проверка в CI с первого гайда.