Сборка и публикация
Приложение
bash
npm run build # tsc --noEmit + сборка в dist/Пути относительные (base: './'), так что подойдёт любой статический хостинг с HTTPS: Vercel, GitHub Pages, Netlify, Cloudflare Pages.
Дальше — в @BotFather:
/newbot— создать бота, если его ещё нет./newapp→ выбрать бота → указать HTTPS-адрес собранного приложения.- Открыть мини-приложение по ссылке от BotFather.
Бот-токен приложению не нужен
Мини-приложение работает целиком на клиенте. Токен нужен только для серверных вызовов Bot API и в бандле стал бы публичным. В репозитории его нет и быть не должно.
Установка на телефон (PWA)
Вне Telegram приложение ставится на домашний экран: public/manifest.webmanifest, иконки и public/sw.js. Внутри Telegram воркер намеренно не регистрируется — причины перечислены в src/main.tsx рядом с условием.
Картинки собираются, а не рисуются:
bash
npm run brand # иконки, фавиконы, превью ссылки для лендингаСкрипт tools/shots/brand.mjs берёт геометрию из src/components/batteryGeometry.ts и рендерит её Chromium'ом — тем же, что уже стоит ради скриншотов. Разъехаться копии не дают tools/brand.test.ts. Перезапись только при изменении, поэтому лишний прогон git status не пачкает.
Пути внутри манифеста относительные
Содержимое manifest.webmanifest Vite не переписывает — это просто копия из public/. Абсолютные пути сломали бы обещание base: './' о том, что сборка переживает размещение в подкаталоге. Ссылки на манифест в index.html можно писать с ведущим слэшем: их Vite приведёт к относительным сам.
Как снять воркер, если придётся
Просто удалить public/sw.js нельзя: уже установленные воркеры продолжат жить у людей на устройствах и отдавать свою копию. Вместо удаления содержимое файла заменяется на снос самого себя:
js
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
for (const name of await caches.keys()) await caches.delete(name);
await self.registration.unregister();
})(),
);
});Версия воркера приходит строкой запроса (sw.js?v=<BUILD_ID>), поэтому новая сборка — это для браузера новый воркер, и снос доедет со следующим заходом.
Документация
Отдельный проект Vercel с Root Directory = docs. Настройки — в docs/vercel.json; собирается vitepress build в .vitepress/dist.
Root Directory обязателен: при нём Vercel ставит зависимости из docs/package.json и не видит корневой. Без него docs-проект прочитал бы корневой vercel.json с framework: vite и собрал бы приложение вместо сайта.
Сборка сайта роняется на любой битой внутренней ссылке (ignoreDeadLinks: false), так что деплой заодно работает проверкой связности.
Даты «Обновлено»
Vercel клонирует репозиторий поверхностно, и даты последнего изменения могут схлопнуться в одну. Лечится добавлением git fetch --unshallow || true в install command проекта. Если возиться не хочется — выключите lastUpdated в docs/.vitepress/config.mts: неверная дата хуже отсутствующей.
Лендинг
Третий отдельный проект Vercel, с Root Directory = landing и по тем же причинам, что и документация. Настройки — в landing/vercel.json.
Корень домена лендингом занимать нельзя, если приложение живёт там же: redirectUri() в src/sync/oauth.ts возвращает ${origin}/, и этот же адрес зарегистрирован в BotFather как адрес возврата. Статическая страница на / молча сломала бы вход через Telegram. Поэтому лендинг — отдельный деплой со своим адресом, а не подкаталог приложения.
Все внешние адреса лежат в landing/site.config.js и подставляются в HTML при сборке метками , , , , . Неизвестная метка роняет сборку: опечатка должна ронять деплой, а не уезжать текстом на прод. Появится свой домен — правится один файл.
Токены оформления и скриншоты лежат в landing/ копиями:
bash
npm run landing:sync # обновить копии из src/styles/tokens.css и docs/public/shots/Копии, а не импорт из ../src, потому что при Root Directory = landing Vercel не видит файлы вне каталога иначе как через галку в дашборде — скрытая связь, которую нельзя выразить в репозитории. Что копии не разъехались, сторожит tools/landing.test.ts при обычном npm test.
Почему кадры не берутся прямо из docs/
tools/docs.test.ts роняет тесты, если в docs/public/shots/ появится PNG, на который нет ссылки из markdown. Лендинг переиспользует только те кадры, что уже описаны в документации, и держит их у себя.
Что не ломается от документации и лендинга
Корневая сборка приложения к docs/, landing/, worker/ и tools/ отношения не имеет:
tsconfig.jsonсодержит"include": ["src"]— проверка типов их не видит;vite buildсобирает от корневогоindex.html;- vitest ограничен
src/**,tools/**иworker/test/**; - ни VitePress, ни Vite лендинга, ни Playwright не лежат в корневом
package.json, поэтомуnpm installпри деплое приложения не изменился.
Перед релизом
bash
npm test # логика, связность документации, копии лендинга, геометрия иконок
npm run build # tsc --noEmit + бандл
npm run docs:shots # два прогона подряд должны дать «Изменилось 0»
npm run brand # то же: второй прогон должен дать «изменилось 0»
npm run devkit:sync # панель отладки для документации и лендинга
npm run docs:build
npm run landing:buildИ чек-лист ручной проверки — то, что автоматика проверить не может.
Отладка на реальном устройстве
bash
npx cloudflared tunnel --url http://localhost:5173Полученный HTTPS-адрес указывается в BotFather как Mini App URL. allowedHosts: true в vite.config.ts уже разрешает произвольный хост туннеля.
Чтобы там же работала панель отладки, к адресу дописывается ?devkit=1: по имени туннеля со случайным хвостом свою машину от боевого адреса не отличить.