Ссылки на сценарии приложения
Настройки интерфейса переживают открытие по отдельному ограниченному договору. Теперь поможем читателю сразу попасть к нужному курсу. Результат урока — полный модуль создания ссылки основного mini app и разбора её сценарного параметра. Ссылка выбирает публичный экран; она не содержит токена, личного плана или разрешения записывать данные. Собственный бот и Main Mini App должны быть отдельно созданы и настроены владельцем.
Мы выбрали Main Mini App в начале серии. Для этого режима официальный формат имеет вид https://t.me/<botusername>?startapp=.... У отдельного именованного приложения есть путь с appname; это другой договор, не случайный дополнительный сегмент нашего URL. Документация Telegram объясняет оба способа. Не меняйте формат в генераторе без изменения настроенного сценария открытия: красивый адрес ещё не гарантирует нужный экран.
Небольшой язык сценариев
Содержимое startapp будем строить из собственных известных значений: course_javascript, course_html_css, course_markdown и course_performance. Такой язык не требует JSON, вложенного URL и персональных данных. Он короток, состоит из букв и подчёркиваний и укладывается в ограниченный сценарий платформы. Мы не используем весь максимальный размер параметра, поэтому расширение каталога сначала обновляет whitelist, а не принимает произвольные длинные строки.
Не передавайте сюда userId, bearer token или описание личного расписания. Ссылка может попасть в сообщение, историю и аналитику переходов. Если в будущем понадобится приглашение к закрытому материалу, потребуется отдельный непрозрачный ограниченный серверный идентификатор и проверка прав. Наш параметр курса не является таким приглашением. Даже после проверки подписи запуска человек мог сам выбрать этот курс в своей ссылке.
Полный модуль ссылок
Создадим course-links.js. Username является настройкой владельца, полученной после регистрации собственного бота. Функция не проверяет, существует ли этот бот и настроено ли у него приложение: это задача отдельной конфигурации. Условные имена в статье нельзя считать доступными адресами или уже зарегистрированными нами аккаунтами.
const values = new Map([
['javascript', 'course_javascript'],
['html-css', 'course_html_css'],
['markdown', 'course_markdown'],
['performance', 'course_performance']
]);
const routes = new Map([...values].map(([courseId, value]) => [value, courseId]));
export function createCourseLink(botUsername, courseId) {
if (typeof botUsername !== 'string' ||
!/^[A-Za-z][A-Za-z0-9_]{4,31}$/.test(botUsername) ||
!botUsername.toLowerCase().endsWith('bot')) {
throw new Error('Configure the owner bot username');
}
const value = values.get(courseId);
if (!value) throw new Error('Unknown public course');
const link = new URL(`https://t.me/${botUsername}`);
link.searchParams.set('startapp', value);
return link.href;
}
export function parseCourseScenario(value) {
return typeof value === 'string' && routes.has(value) ? routes.get(value) : null;
}
Генератор создаёт URL через URL и searchParams, а не конкатенирует произвольную строку запроса от пользователя. Вход courseId совпадает с доменом формы; неизвестный курс приводит к явной ошибке. Парсер возвращает null для пустого, изменённого или старого значения, сохраняя нормальный каталог. Он не создаёт карточку с именем из URL и не выполняет redirect на переданный адрес. Добавление нового курса требует согласованного изменения карты и каталога.
Быстрая подсказка и подтверждённый контекст
При открытии значение может появиться в GET tgWebAppStartParam, а также в данных запуска Telegram. GET пригоден для раннего выбора публичной оболочки, но не считается подтверждённым серверным контекстом. В модели lessons восемь и девять raw initData отправляется без пересборки, затем сервер проверяет HMAC. Подтверждённый startParam можно разобрать тем же parseCourseScenario, однако такой результат всё равно остаётся выбранным направлением, а не правилом доступа.
Если GET показывает Markdown, а подтверждённый контекст другое значение, приложение действует по заранее выбранной политике. В нашей серии персональная операция вообще не зависит от стартового курса: она использует текущий draft и владельца сессии. Для навигации можно принять подтверждённый известный вариант после обмена, пока человек ещё не изменил маршрут, либо оставить уже выбранный экран. Важна согласованность: поздний ответ не должен внезапно перескочить на другой курс во время заполнения формы.
Ссылка не заменяет состояние
Открытие course_markdown показывает детали Markdown и может предложить этот курс как новый выбор. Если на сервере уже сохранён JavaScript, его нельзя незаметно заменить только по факту перехода. Base читается отдельно, а draft меняется по явному действию либо по чётко обозначенному предзаполнению. Интерфейс объясняет: «Открыт курс Markdown; сохранённый план пока другой». Тогда человек понимает различие между переходом и подтверждённой записью.
Возврат работает через ту же навигационную модель двух экранов. На неизвестном параметре показывается каталог и короткое объяснение отсутствующего сценария. Не стоит показывать технический код целиком в общем сообщении, особенно если параметр был слишком длинным. Нормальное состояние каталога остаётся полезным результатом даже у устаревшей ссылки из старого сообщения. Для известных старых обозначений можно добавить конкретный alias, но не универсальный поиск совпадения по части строки.
Измерение перехода без секретов
Если продукту нужен источник посещения, не смешивайте его с владельцем. Разрешённые campaign codes должны иметь свой ограниченный формат и перечень, а персональные данные не дописываются в startapp для удобства аналитики. В нашем минимальном модуле campaign отсутствует: он решает только навигацию по четырём курсам. Журнал может записать имя известного сценария и результат разбора, не raw initData и не токен обмена.
Проверьте ожидаемые варианты отдельно: известный JavaScript создаёт один HTTPS t.me адрес; html-css переводится в course_html_css; неизвестный ключ отклоняется; изменённый параметр возвращает null; обычная браузерная ссылка без Telegram не создаёт сессию. Для последнего сценария публичная оболочка остаётся доступной, но персональная запись требует собственного серверного обмена. Telegram сообщает, что прямой запуск не даёт приложению автоматически читать или отправлять сообщения от имени человека.
Сохранение внешнего договора
После выпуска значение startapp становится частью публичных сообщений. Переименование course_markdown в новый код способно сломать старые приглашения даже при сохранённом веб-адресе. Поэтому на изменении каталога храните таблицу известных внешних обозначений и их назначений. Если курс снят, полезнее объяснить отсутствие и показать альтернативу, чем вернуть неопределённый экран. Это похожая задача на сохранение старых URL веб-сайта, хотя механизм маршрутизации другой.
Ожидаемый итог — ссылка выбирает один публичный маршрут и не подменяет личный план. Ни бот, ни t.me переходы, ни Telegram-клиенты здесь не запускались; существование и настройки конкретного username не подтверждены. Модуль работает с переданной конфигурацией и не регистрирует приложение. Дальше дадим человеку действие «поделиться курсом», в котором он сам выбирает получателя и отправку, а интерфейс не объявляет сообщение доставленным только после открытия окна.