[{"data":1,"prerenderedAt":1822},["ShallowReactive",2],{"header-latest":3,"sitemap-content":1368},[4],{"id":5,"title":6,"body":7,"date":1342,"description":1343,"extension":1344,"faq":1345,"image":1355,"lang":1356,"meta":1357,"navigation":106,"path":1358,"published":106,"readTime":213,"seo":1359,"stem":1360,"tags":1361,"updated":1342,"__hash__":1367},"blog\u002Fblog\u002Fru\u002Ftelegram-web-apps-sells.md","Telegram Web Apps (TWA): Как запустить полноценный SaaS внутри Telegram",{"type":8,"value":9,"toc":1331},"minimark",[10,19,22,25,30,33,44,51,61,68,71,854,858,861,866,872,920,924,927,1212,1216,1225,1228,1266,1279,1287,1291,1311,1314,1327],[11,12,13,14,18],"p",{},"Telegram давно перешагнул рамки классического мессенджера для обмена сообщениями, превратившись в полноценную кроссплатформенную среду выполнения приложений. С появлением технологии ",[15,16,17],"strong",{},"Telegram Web Apps (TWA)",", также известной как Telegram Mini Apps, разработчики могут запускать интерактивные SPA-приложения прямо внутри интерфейса мессенджера.",[11,20,21],{},"Это дает стартапам и бизнесу мгновенный бесшовный доступ к аудитории более 900 миллионов активных пользователей. Размещая свой B2B SaaS, личный кабинет CRM или бизнес-инструмент в боте Telegram, вы устраняете классические барьеры воронки: необходимость скачивать приложения из сторов, регистрироваться и авторизовываться на сторонних сайтах.",[11,23,24],{},"В этом техническом руководстве мы разберем архитектуру TWA, напишем безопасный бэкенд-валидатор на Node.js\u002FTypeScript и настроим синхронизацию стилей на фронтенде.",[26,27,29],"h2",{"id":28},"архитектура-экосистемы-telegram-mini-apps","Архитектура экосистемы Telegram Mini Apps",[11,31,32],{},"В отличие от стандартных веб-приложений, TWA имеет двухстороннюю связь с клиентом. Фронтенд работает в изолированном компоненте WebView, контролируемом клиентом Telegram, в то время как бэкенд взаимодействует как с API Telegram Bot, так и с базой данных приложения.",[34,35,41],"pre",{"className":36,"code":38,"language":39,"meta":40},[37],"language-text","+-----------------------------------------------------------+\n|                     Клиент Telegram                       |\n|  +-----------------------------------------------------+  |\n|  |                 Mini App (WebView UI)               |  |\n|  |     (Использует window.Telegram.WebApp SDK)         |  |\n|  +--------------------------+--------------------------+  |\n+-----------------------------|-----------------------------+\n       | (initData параметры) | (Защищенный HTTPS-запрос)\n       v                      v\n+--------------+       +------------------------------------+\n| Telegram API |       |            Ваш Бэкенд              |\n|  (Вебхуки)   |       | (Проверяет подпись через HMAC-256) |\n+------+-------+       +-----------------+------------------+\n       |                                 |\n       +----------------> [Синхр. БД] \u003C---+\n","text","",[42,43,38],"code",{"__ignoreMap":40},[26,45,47,48],{"id":46},"_1-безопасность-валидация-строки-initdata","1. Безопасность: Валидация строки ",[42,49,50],{},"initData",[11,52,53,54,56,57,60],{},"Когда пользователь открывает Mini App, Telegram передает на фронтенд строку параметров ",[42,55,50],{}," (содержащую данные профиля пользователя, тему оформления, контекст запуска) и защитную подпись ",[42,58,59],{},"hash",".",[11,62,63,64,67],{},"Чтобы злоумышленники не могли подделать идентификатор пользователя (User ID) или эмулировать оплату тарифа, вы ",[15,65,66],{},"обязаны валидировать этот хэш на вашем бэкенде"," с помощью токена вашего Telegram-бота.",[11,69,70],{},"Вот реализация валидации на TypeScript\u002FNode.js:",[34,72,76],{"className":73,"code":74,"language":75,"meta":40,"style":40},"language-typescript shiki shiki-themes github-dark","import crypto from 'crypto';\n\ninterface TelegramUserData {\n  id: number;\n  first_name: string;\n  last_name?: string;\n  username?: string;\n  language_code?: string;\n  is_premium?: boolean;\n}\n\ninterface ValidationResult {\n  isValid: boolean;\n  user?: TelegramUserData;\n}\n\nexport function verifyTelegramInitData(rawQueryString: string, botToken: string): ValidationResult {\n  const urlParams = new URLSearchParams(rawQueryString);\n  const hash = urlParams.get('hash');\n  \n  if (!hash) {\n    return { isValid: false };\n  }\n\n  \u002F\u002F 1. Сортируем все переданные ключи в алфавитном порядке, кроме самого hash\n  const keys = Array.from(urlParams.keys()).filter(key => key !== 'hash').sort();\n  \n  \u002F\u002F 2. Восстанавливаем проверочную строку данных\n  const dataCheckString = keys\n    .map(key => `${key}=${urlParams.get(key)}`)\n    .join('\\n');\n\n  \u002F\u002F 3. Создаем секретный ключ\n  \u002F\u002F Мы используем константную строку \"WebAppData\" для шифрования токена бота\n  const secretKey = crypto\n    .createHmac('sha256', 'WebAppData')\n    .update(botToken)\n    .digest();\n\n  \u002F\u002F 4. Вычисляем хэш-сигнатуру от проверочной строки\n  const computedHash = crypto\n    .createHmac('sha256', secretKey)\n    .update(dataCheckString)\n    .digest('hex');\n\n  \u002F\u002F 5. Безопасно сравниваем вычисленный хэш с тем, который прислал клиент\n  const isValid = computedHash === hash;\n\n  if (!isValid) {\n    return { isValid: false };\n  }\n\n  \u002F\u002F Десериализуем объект пользователя, если валидация прошла успешно\n  try {\n    const userRaw = urlParams.get('user');\n    const user: TelegramUserData = userRaw ? JSON.parse(userRaw) : undefined;\n    return { isValid: true, user };\n  } catch (error) {\n    return { isValid: true };\n  }\n}\n","typescript",[42,77,78,101,108,121,137,150,163,175,187,200,206,211,221,233,245,250,255,296,317,341,347,362,377,383,388,395,448,453,459,472,514,534,539,545,551,564,584,595,605,610,616,628,642,652,666,671,677,696,701,713,724,729,734,740,748,770,808,821,833,844,849],{"__ignoreMap":40},[79,80,83,87,91,94,98],"span",{"class":81,"line":82},"line",1,[79,84,86],{"class":85},"snl16","import",[79,88,90],{"class":89},"s95oV"," crypto ",[79,92,93],{"class":85},"from",[79,95,97],{"class":96},"sU2Wk"," 'crypto'",[79,99,100],{"class":89},";\n",[79,102,104],{"class":81,"line":103},2,[79,105,107],{"emptyLinePlaceholder":106},true,"\n",[79,109,111,114,118],{"class":81,"line":110},3,[79,112,113],{"class":85},"interface",[79,115,117],{"class":116},"svObZ"," TelegramUserData",[79,119,120],{"class":89}," {\n",[79,122,124,128,131,135],{"class":81,"line":123},4,[79,125,127],{"class":126},"s9osk","  id",[79,129,130],{"class":85},":",[79,132,134],{"class":133},"sDLfK"," number",[79,136,100],{"class":89},[79,138,140,143,145,148],{"class":81,"line":139},5,[79,141,142],{"class":126},"  first_name",[79,144,130],{"class":85},[79,146,147],{"class":133}," string",[79,149,100],{"class":89},[79,151,153,156,159,161],{"class":81,"line":152},6,[79,154,155],{"class":126},"  last_name",[79,157,158],{"class":85},"?:",[79,160,147],{"class":133},[79,162,100],{"class":89},[79,164,166,169,171,173],{"class":81,"line":165},7,[79,167,168],{"class":126},"  username",[79,170,158],{"class":85},[79,172,147],{"class":133},[79,174,100],{"class":89},[79,176,178,181,183,185],{"class":81,"line":177},8,[79,179,180],{"class":126},"  language_code",[79,182,158],{"class":85},[79,184,147],{"class":133},[79,186,100],{"class":89},[79,188,190,193,195,198],{"class":81,"line":189},9,[79,191,192],{"class":126},"  is_premium",[79,194,158],{"class":85},[79,196,197],{"class":133}," boolean",[79,199,100],{"class":89},[79,201,203],{"class":81,"line":202},10,[79,204,205],{"class":89},"}\n",[79,207,209],{"class":81,"line":208},11,[79,210,107],{"emptyLinePlaceholder":106},[79,212,214,216,219],{"class":81,"line":213},12,[79,215,113],{"class":85},[79,217,218],{"class":116}," ValidationResult",[79,220,120],{"class":89},[79,222,224,227,229,231],{"class":81,"line":223},13,[79,225,226],{"class":126},"  isValid",[79,228,130],{"class":85},[79,230,197],{"class":133},[79,232,100],{"class":89},[79,234,236,239,241,243],{"class":81,"line":235},14,[79,237,238],{"class":126},"  user",[79,240,158],{"class":85},[79,242,117],{"class":116},[79,244,100],{"class":89},[79,246,248],{"class":81,"line":247},15,[79,249,205],{"class":89},[79,251,253],{"class":81,"line":252},16,[79,254,107],{"emptyLinePlaceholder":106},[79,256,258,261,264,267,270,273,275,277,280,283,285,287,290,292,294],{"class":81,"line":257},17,[79,259,260],{"class":85},"export",[79,262,263],{"class":85}," function",[79,265,266],{"class":116}," verifyTelegramInitData",[79,268,269],{"class":89},"(",[79,271,272],{"class":126},"rawQueryString",[79,274,130],{"class":85},[79,276,147],{"class":133},[79,278,279],{"class":89},", ",[79,281,282],{"class":126},"botToken",[79,284,130],{"class":85},[79,286,147],{"class":133},[79,288,289],{"class":89},")",[79,291,130],{"class":85},[79,293,218],{"class":116},[79,295,120],{"class":89},[79,297,299,302,305,308,311,314],{"class":81,"line":298},18,[79,300,301],{"class":85},"  const",[79,303,304],{"class":133}," urlParams",[79,306,307],{"class":85}," =",[79,309,310],{"class":85}," new",[79,312,313],{"class":116}," URLSearchParams",[79,315,316],{"class":89},"(rawQueryString);\n",[79,318,320,322,325,327,330,333,335,338],{"class":81,"line":319},19,[79,321,301],{"class":85},[79,323,324],{"class":133}," hash",[79,326,307],{"class":85},[79,328,329],{"class":89}," urlParams.",[79,331,332],{"class":116},"get",[79,334,269],{"class":89},[79,336,337],{"class":96},"'hash'",[79,339,340],{"class":89},");\n",[79,342,344],{"class":81,"line":343},20,[79,345,346],{"class":89},"  \n",[79,348,350,353,356,359],{"class":81,"line":349},21,[79,351,352],{"class":85},"  if",[79,354,355],{"class":89}," (",[79,357,358],{"class":85},"!",[79,360,361],{"class":89},"hash) {\n",[79,363,365,368,371,374],{"class":81,"line":364},22,[79,366,367],{"class":85},"    return",[79,369,370],{"class":89}," { isValid: ",[79,372,373],{"class":133},"false",[79,375,376],{"class":89}," };\n",[79,378,380],{"class":81,"line":379},23,[79,381,382],{"class":89},"  }\n",[79,384,386],{"class":81,"line":385},24,[79,387,107],{"emptyLinePlaceholder":106},[79,389,391],{"class":81,"line":390},25,[79,392,394],{"class":393},"sAwPA","  \u002F\u002F 1. Сортируем все переданные ключи в алфавитном порядке, кроме самого hash\n",[79,396,398,400,403,405,408,410,413,416,419,422,424,427,430,433,436,439,442,445],{"class":81,"line":397},26,[79,399,301],{"class":85},[79,401,402],{"class":133}," keys",[79,404,307],{"class":85},[79,406,407],{"class":89}," Array.",[79,409,93],{"class":116},[79,411,412],{"class":89},"(urlParams.",[79,414,415],{"class":116},"keys",[79,417,418],{"class":89},"()).",[79,420,421],{"class":116},"filter",[79,423,269],{"class":89},[79,425,426],{"class":126},"key",[79,428,429],{"class":85}," =>",[79,431,432],{"class":89}," key ",[79,434,435],{"class":85},"!==",[79,437,438],{"class":96}," 'hash'",[79,440,441],{"class":89},").",[79,443,444],{"class":116},"sort",[79,446,447],{"class":89},"();\n",[79,449,451],{"class":81,"line":450},27,[79,452,346],{"class":89},[79,454,456],{"class":81,"line":455},28,[79,457,458],{"class":393},"  \u002F\u002F 2. Восстанавливаем проверочную строку данных\n",[79,460,462,464,467,469],{"class":81,"line":461},29,[79,463,301],{"class":85},[79,465,466],{"class":133}," dataCheckString",[79,468,307],{"class":85},[79,470,471],{"class":89}," keys\n",[79,473,475,478,481,483,485,487,490,492,495,498,500,502,504,506,508,511],{"class":81,"line":474},30,[79,476,477],{"class":89},"    .",[79,479,480],{"class":116},"map",[79,482,269],{"class":89},[79,484,426],{"class":126},[79,486,429],{"class":85},[79,488,489],{"class":96}," `${",[79,491,426],{"class":89},[79,493,494],{"class":96},"}=${",[79,496,497],{"class":89},"urlParams",[79,499,60],{"class":96},[79,501,332],{"class":116},[79,503,269],{"class":96},[79,505,426],{"class":89},[79,507,289],{"class":96},[79,509,510],{"class":96},"}`",[79,512,513],{"class":89},")\n",[79,515,517,519,522,524,527,530,532],{"class":81,"line":516},31,[79,518,477],{"class":89},[79,520,521],{"class":116},"join",[79,523,269],{"class":89},[79,525,526],{"class":96},"'",[79,528,529],{"class":133},"\\n",[79,531,526],{"class":96},[79,533,340],{"class":89},[79,535,537],{"class":81,"line":536},32,[79,538,107],{"emptyLinePlaceholder":106},[79,540,542],{"class":81,"line":541},33,[79,543,544],{"class":393},"  \u002F\u002F 3. Создаем секретный ключ\n",[79,546,548],{"class":81,"line":547},34,[79,549,550],{"class":393},"  \u002F\u002F Мы используем константную строку \"WebAppData\" для шифрования токена бота\n",[79,552,554,556,559,561],{"class":81,"line":553},35,[79,555,301],{"class":85},[79,557,558],{"class":133}," secretKey",[79,560,307],{"class":85},[79,562,563],{"class":89}," crypto\n",[79,565,567,569,572,574,577,579,582],{"class":81,"line":566},36,[79,568,477],{"class":89},[79,570,571],{"class":116},"createHmac",[79,573,269],{"class":89},[79,575,576],{"class":96},"'sha256'",[79,578,279],{"class":89},[79,580,581],{"class":96},"'WebAppData'",[79,583,513],{"class":89},[79,585,587,589,592],{"class":81,"line":586},37,[79,588,477],{"class":89},[79,590,591],{"class":116},"update",[79,593,594],{"class":89},"(botToken)\n",[79,596,598,600,603],{"class":81,"line":597},38,[79,599,477],{"class":89},[79,601,602],{"class":116},"digest",[79,604,447],{"class":89},[79,606,608],{"class":81,"line":607},39,[79,609,107],{"emptyLinePlaceholder":106},[79,611,613],{"class":81,"line":612},40,[79,614,615],{"class":393},"  \u002F\u002F 4. Вычисляем хэш-сигнатуру от проверочной строки\n",[79,617,619,621,624,626],{"class":81,"line":618},41,[79,620,301],{"class":85},[79,622,623],{"class":133}," computedHash",[79,625,307],{"class":85},[79,627,563],{"class":89},[79,629,631,633,635,637,639],{"class":81,"line":630},42,[79,632,477],{"class":89},[79,634,571],{"class":116},[79,636,269],{"class":89},[79,638,576],{"class":96},[79,640,641],{"class":89},", secretKey)\n",[79,643,645,647,649],{"class":81,"line":644},43,[79,646,477],{"class":89},[79,648,591],{"class":116},[79,650,651],{"class":89},"(dataCheckString)\n",[79,653,655,657,659,661,664],{"class":81,"line":654},44,[79,656,477],{"class":89},[79,658,602],{"class":116},[79,660,269],{"class":89},[79,662,663],{"class":96},"'hex'",[79,665,340],{"class":89},[79,667,669],{"class":81,"line":668},45,[79,670,107],{"emptyLinePlaceholder":106},[79,672,674],{"class":81,"line":673},46,[79,675,676],{"class":393},"  \u002F\u002F 5. Безопасно сравниваем вычисленный хэш с тем, который прислал клиент\n",[79,678,680,682,685,687,690,693],{"class":81,"line":679},47,[79,681,301],{"class":85},[79,683,684],{"class":133}," isValid",[79,686,307],{"class":85},[79,688,689],{"class":89}," computedHash ",[79,691,692],{"class":85},"===",[79,694,695],{"class":89}," hash;\n",[79,697,699],{"class":81,"line":698},48,[79,700,107],{"emptyLinePlaceholder":106},[79,702,704,706,708,710],{"class":81,"line":703},49,[79,705,352],{"class":85},[79,707,355],{"class":89},[79,709,358],{"class":85},[79,711,712],{"class":89},"isValid) {\n",[79,714,716,718,720,722],{"class":81,"line":715},50,[79,717,367],{"class":85},[79,719,370],{"class":89},[79,721,373],{"class":133},[79,723,376],{"class":89},[79,725,727],{"class":81,"line":726},51,[79,728,382],{"class":89},[79,730,732],{"class":81,"line":731},52,[79,733,107],{"emptyLinePlaceholder":106},[79,735,737],{"class":81,"line":736},53,[79,738,739],{"class":393},"  \u002F\u002F Десериализуем объект пользователя, если валидация прошла успешно\n",[79,741,743,746],{"class":81,"line":742},54,[79,744,745],{"class":85},"  try",[79,747,120],{"class":89},[79,749,751,754,757,759,761,763,765,768],{"class":81,"line":750},55,[79,752,753],{"class":85},"    const",[79,755,756],{"class":133}," userRaw",[79,758,307],{"class":85},[79,760,329],{"class":89},[79,762,332],{"class":116},[79,764,269],{"class":89},[79,766,767],{"class":96},"'user'",[79,769,340],{"class":89},[79,771,773,775,778,780,782,784,787,790,793,795,798,801,803,806],{"class":81,"line":772},56,[79,774,753],{"class":85},[79,776,777],{"class":133}," user",[79,779,130],{"class":85},[79,781,117],{"class":116},[79,783,307],{"class":85},[79,785,786],{"class":89}," userRaw ",[79,788,789],{"class":85},"?",[79,791,792],{"class":133}," JSON",[79,794,60],{"class":89},[79,796,797],{"class":116},"parse",[79,799,800],{"class":89},"(userRaw) ",[79,802,130],{"class":85},[79,804,805],{"class":133}," undefined",[79,807,100],{"class":89},[79,809,811,813,815,818],{"class":81,"line":810},57,[79,812,367],{"class":85},[79,814,370],{"class":89},[79,816,817],{"class":133},"true",[79,819,820],{"class":89},", user };\n",[79,822,824,827,830],{"class":81,"line":823},58,[79,825,826],{"class":89},"  } ",[79,828,829],{"class":85},"catch",[79,831,832],{"class":89}," (error) {\n",[79,834,836,838,840,842],{"class":81,"line":835},59,[79,837,367],{"class":85},[79,839,370],{"class":89},[79,841,817],{"class":133},[79,843,376],{"class":89},[79,845,847],{"class":81,"line":846},60,[79,848,382],{"class":89},[79,850,852],{"class":81,"line":851},61,[79,853,205],{"class":89},[26,855,857],{"id":856},"_2-интеграция-во-фронтенд-и-адаптация-темы-оформления","2. Интеграция во фронтенд и адаптация темы оформления",[11,859,860],{},"Для создания премиального интерфейса (UI\u002FUX) приложение должно подстраиваться под цветовую гамму клиента Telegram (темная\u002Fсветлая тема). Доступ к этим параметрам осуществляется через библиотеку JS-моста.",[862,863,865],"h3",{"id":864},"шаг-1-подключение-скрипта-в-nuxt-3-html","Шаг 1: Подключение скрипта в Nuxt 3 \u002F HTML",[11,867,868,869,130],{},"Добавьте скрипт в секцию head в Nuxt с помощью composable ",[42,870,871],{},"useHead",[34,873,875],{"className":73,"code":874,"language":75,"meta":40,"style":40},"\u002F\u002F app.vue или layout страницы\nuseHead({\n  script: [\n    { src: 'https:\u002F\u002Ftelegram.org\u002Fjs\u002Ftelegram-web-app.js', defer: true }\n  ]\n})\n",[42,876,877,882,889,894,910,915],{"__ignoreMap":40},[79,878,879],{"class":81,"line":82},[79,880,881],{"class":393},"\u002F\u002F app.vue или layout страницы\n",[79,883,884,886],{"class":81,"line":103},[79,885,871],{"class":116},[79,887,888],{"class":89},"({\n",[79,890,891],{"class":81,"line":110},[79,892,893],{"class":89},"  script: [\n",[79,895,896,899,902,905,907],{"class":81,"line":123},[79,897,898],{"class":89},"    { src: ",[79,900,901],{"class":96},"'https:\u002F\u002Ftelegram.org\u002Fjs\u002Ftelegram-web-app.js'",[79,903,904],{"class":89},", defer: ",[79,906,817],{"class":133},[79,908,909],{"class":89}," }\n",[79,911,912],{"class":81,"line":139},[79,913,914],{"class":89},"  ]\n",[79,916,917],{"class":81,"line":152},[79,918,919],{"class":89},"})\n",[862,921,923],{"id":922},"шаг-2-использование-js-моста-в-vue-компонентах","Шаг 2: Использование JS-моста в Vue-компонентах",[11,925,926],{},"Создадим реактивный composable для считывания настроек Telegram:",[34,928,930],{"className":73,"code":929,"language":75,"meta":40,"style":40},"\u002F\u002F composables\u002FuseTelegram.ts\nimport { ref, onMounted } from 'vue';\n\nexport function useTelegram() {\n  const isReady = ref(false);\n  const user = ref\u003Cany>(null);\n\n  onMounted(() => {\n    const tg = (window as any).Telegram?.WebApp;\n    if (tg) {\n      tg.ready();\n      tg.expand(); \u002F\u002F Разворачиваем окно на максимальную высоту\n      \n      user.value = tg.initDataUnsafe?.user;\n      isReady.value = true;\n\n      \u002F\u002F Передаем цвета темы Telegram в переменные CSS (Custom Properties)\n      const root = document.documentElement;\n      root.style.setProperty('--color-tg-bg', tg.themeParams.bg_color);\n      root.style.setProperty('--color-tg-text', tg.themeParams.text_color);\n      root.style.setProperty('--color-tg-button', tg.themeParams.button_color);\n      root.style.setProperty('--color-tg-button-text', tg.themeParams.button_text_color);\n    }\n  });\n\n  return { isReady, user };\n}\n",[42,931,932,937,951,955,967,985,1009,1013,1026,1047,1055,1065,1078,1083,1094,1106,1110,1115,1128,1144,1158,1172,1186,1191,1196,1200,1208],{"__ignoreMap":40},[79,933,934],{"class":81,"line":82},[79,935,936],{"class":393},"\u002F\u002F composables\u002FuseTelegram.ts\n",[79,938,939,941,944,946,949],{"class":81,"line":103},[79,940,86],{"class":85},[79,942,943],{"class":89}," { ref, onMounted } ",[79,945,93],{"class":85},[79,947,948],{"class":96}," 'vue'",[79,950,100],{"class":89},[79,952,953],{"class":81,"line":110},[79,954,107],{"emptyLinePlaceholder":106},[79,956,957,959,961,964],{"class":81,"line":123},[79,958,260],{"class":85},[79,960,263],{"class":85},[79,962,963],{"class":116}," useTelegram",[79,965,966],{"class":89},"() {\n",[79,968,969,971,974,976,979,981,983],{"class":81,"line":139},[79,970,301],{"class":85},[79,972,973],{"class":133}," isReady",[79,975,307],{"class":85},[79,977,978],{"class":116}," ref",[79,980,269],{"class":89},[79,982,373],{"class":133},[79,984,340],{"class":89},[79,986,987,989,991,993,995,998,1001,1004,1007],{"class":81,"line":152},[79,988,301],{"class":85},[79,990,777],{"class":133},[79,992,307],{"class":85},[79,994,978],{"class":116},[79,996,997],{"class":89},"\u003C",[79,999,1000],{"class":133},"any",[79,1002,1003],{"class":89},">(",[79,1005,1006],{"class":133},"null",[79,1008,340],{"class":89},[79,1010,1011],{"class":81,"line":165},[79,1012,107],{"emptyLinePlaceholder":106},[79,1014,1015,1018,1021,1024],{"class":81,"line":177},[79,1016,1017],{"class":116},"  onMounted",[79,1019,1020],{"class":89},"(() ",[79,1022,1023],{"class":85},"=>",[79,1025,120],{"class":89},[79,1027,1028,1030,1033,1035,1038,1041,1044],{"class":81,"line":189},[79,1029,753],{"class":85},[79,1031,1032],{"class":133}," tg",[79,1034,307],{"class":85},[79,1036,1037],{"class":89}," (window ",[79,1039,1040],{"class":85},"as",[79,1042,1043],{"class":133}," any",[79,1045,1046],{"class":89},").Telegram?.WebApp;\n",[79,1048,1049,1052],{"class":81,"line":202},[79,1050,1051],{"class":85},"    if",[79,1053,1054],{"class":89}," (tg) {\n",[79,1056,1057,1060,1063],{"class":81,"line":208},[79,1058,1059],{"class":89},"      tg.",[79,1061,1062],{"class":116},"ready",[79,1064,447],{"class":89},[79,1066,1067,1069,1072,1075],{"class":81,"line":213},[79,1068,1059],{"class":89},[79,1070,1071],{"class":116},"expand",[79,1073,1074],{"class":89},"(); ",[79,1076,1077],{"class":393},"\u002F\u002F Разворачиваем окно на максимальную высоту\n",[79,1079,1080],{"class":81,"line":223},[79,1081,1082],{"class":89},"      \n",[79,1084,1085,1088,1091],{"class":81,"line":235},[79,1086,1087],{"class":89},"      user.value ",[79,1089,1090],{"class":85},"=",[79,1092,1093],{"class":89}," tg.initDataUnsafe?.user;\n",[79,1095,1096,1099,1101,1104],{"class":81,"line":247},[79,1097,1098],{"class":89},"      isReady.value ",[79,1100,1090],{"class":85},[79,1102,1103],{"class":133}," true",[79,1105,100],{"class":89},[79,1107,1108],{"class":81,"line":252},[79,1109,107],{"emptyLinePlaceholder":106},[79,1111,1112],{"class":81,"line":257},[79,1113,1114],{"class":393},"      \u002F\u002F Передаем цвета темы Telegram в переменные CSS (Custom Properties)\n",[79,1116,1117,1120,1123,1125],{"class":81,"line":298},[79,1118,1119],{"class":85},"      const",[79,1121,1122],{"class":133}," root",[79,1124,307],{"class":85},[79,1126,1127],{"class":89}," document.documentElement;\n",[79,1129,1130,1133,1136,1138,1141],{"class":81,"line":319},[79,1131,1132],{"class":89},"      root.style.",[79,1134,1135],{"class":116},"setProperty",[79,1137,269],{"class":89},[79,1139,1140],{"class":96},"'--color-tg-bg'",[79,1142,1143],{"class":89},", tg.themeParams.bg_color);\n",[79,1145,1146,1148,1150,1152,1155],{"class":81,"line":343},[79,1147,1132],{"class":89},[79,1149,1135],{"class":116},[79,1151,269],{"class":89},[79,1153,1154],{"class":96},"'--color-tg-text'",[79,1156,1157],{"class":89},", tg.themeParams.text_color);\n",[79,1159,1160,1162,1164,1166,1169],{"class":81,"line":349},[79,1161,1132],{"class":89},[79,1163,1135],{"class":116},[79,1165,269],{"class":89},[79,1167,1168],{"class":96},"'--color-tg-button'",[79,1170,1171],{"class":89},", tg.themeParams.button_color);\n",[79,1173,1174,1176,1178,1180,1183],{"class":81,"line":364},[79,1175,1132],{"class":89},[79,1177,1135],{"class":116},[79,1179,269],{"class":89},[79,1181,1182],{"class":96},"'--color-tg-button-text'",[79,1184,1185],{"class":89},", tg.themeParams.button_text_color);\n",[79,1187,1188],{"class":81,"line":379},[79,1189,1190],{"class":89},"    }\n",[79,1192,1193],{"class":81,"line":385},[79,1194,1195],{"class":89},"  });\n",[79,1197,1198],{"class":81,"line":390},[79,1199,107],{"emptyLinePlaceholder":106},[79,1201,1202,1205],{"class":81,"line":397},[79,1203,1204],{"class":85},"  return",[79,1206,1207],{"class":89}," { isReady, user };\n",[79,1209,1210],{"class":81,"line":450},[79,1211,205],{"class":89},[26,1213,1215],{"id":1214},"_3-прием-платежей-через-telegram-stars","3. Прием платежей через Telegram Stars",[11,1217,1218,1219,355,1222,441],{},"При продаже цифровых товаров (подписки на SaaS, баланс лимитов ИИ, инфопродукты) правила Apple\u002FGoogle обязывают использовать внутреннюю валюту мессенджера — ",[15,1220,1221],{},"Telegram Stars",[42,1223,1224],{},"XTR",[11,1226,1227],{},"Алгоритм покупки цифрового товара:",[1229,1230,1231,1238,1250,1256],"ol",{},[1232,1233,1234,1237],"li",{},[15,1235,1236],{},"Создание счета",": TWA запрашивает бэкенд сгенерировать счет на оплату.",[1232,1239,1240,1243,1244,1247,1248,60],{},[15,1241,1242],{},"Отправка инвойса",": Бэкенд делает запрос к Bot API ",[42,1245,1246],{},"sendInvoice"," с валютой ",[42,1249,1224],{},[1232,1251,1252,1255],{},[15,1253,1254],{},"Оплата в клиенте",": Telegram открывает нативный экран оплаты, где пользователь списывает «Звезды».",[1232,1257,1258,1261,1262,1265],{},[15,1259,1260],{},"Синхронизация",": Приходит вебхук ",[42,1263,1264],{},"successful_payment"," на бэкенд бота, после чего лимиты зачисляются в вашей базе данных.",[11,1267,1268,1269,1274,1275,60],{},"В моей платформе конструктора ботов ",[1270,1271,1273],"a",{"href":1272},"\u002Fru\u002Fprojects\u002Ftelego","TeleGo.io"," мы предоставляем именно такие встроенные платежные сценарии, позволяя пользователям создавать свои TWA и сразу принимать Stars. Кроме того, мы поддерживаем гибридные хелпдески поддержки, о настройке которых вы можете узнать в статье ",[1270,1276,1278],{"href":1277},"\u002Fru\u002Fblog\u002Ftelegram-bot-ai-rag-support","Как создать ИИ-бота поддержки с RAG",[11,1280,1281,1282,1286],{},"Для физических товаров и консультаций можно использовать классический биллинг. Читайте мое руководство ",[1270,1283,1285],{"href":1284},"\u002Fru\u002Fblog\u002Fsaas-stripe-billing-integration","Монетизация SaaS и интеграция Stripe"," для ознакомления со скриптами Express\u002FTypeScript.",[26,1288,1290],{"id":1289},"источники-и-документация","Источники и документация",[1292,1293,1294,1303],"ul",{},[1232,1295,1296,1302],{},[1270,1297,1301],{"href":1298,"rel":1299},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fwebapps",[1300],"nofollow","Telegram Mini Apps"," — официальная документация WebApp SDK и initData",[1232,1304,1305,1310],{},[1270,1306,1309],{"href":1307,"rel":1308},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fpayments",[1300],"Bot Payments API"," — счета, Telegram Stars и вебхуки оплат",[11,1312,1313],{},"Сочетание современных веб-фронтендов с API Telegram WebApp позволяет создавать удобные B2B-продукты внутри популярного мессенджера, минуя сложные регистрационные формы.",[11,1315,1316,1317,1321,1322,1326],{},"Если вы планируете разработку Telegram Mini App с платежами Stars\u002FStripe и динамической темой оформления, ознакомьтесь с моей услугой ",[1270,1318,1320],{"href":1319},"\u002Fru\u002Ftelegram-bots","Создание Telegram-ботов"," или запишитесь на ",[1270,1323,1325],{"href":1324},"\u002Fru\u002Fconsultations","Консультацию",", чтобы разработать архитектуру.",[1328,1329,1330],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":40,"searchDepth":103,"depth":103,"links":1332},[1333,1334,1336,1340,1341],{"id":28,"depth":103,"text":29},{"id":46,"depth":103,"text":1335},"1. Безопасность: Валидация строки initData",{"id":856,"depth":103,"text":857,"children":1337},[1338,1339],{"id":864,"depth":110,"text":865},{"id":922,"depth":110,"text":923},{"id":1214,"depth":103,"text":1215},{"id":1289,"depth":103,"text":1290},"2026-07-05","Детальное руководство для разработчиков по использованию Telegram Mini Apps в качестве веб-фронтенда, криптографической валидации данных и приему платежей.","md",[1346,1349,1352],{"q":1347,"a":1348},"Чем Telegram Mini App отличается от обычного бота?","Бот общается сообщениями и кнопками, а Mini App открывает внутри Telegram полноценный веб-интерфейс: дашборды, каталоги, формы, графики. Это SPA-приложение в WebView со своим фронтендом, при этом авторизация происходит автоматически через аккаунт Telegram.",{"q":1350,"a":1351},"Как проверить, что данные пользователя в Mini App не подделаны?","Через криптографическую валидацию initData: Telegram подписывает данные пользователя HMAC-SHA256-подписью на основе токена бота. Бэкенд обязан пересчитать и сверить подпись при каждом запросе — без этого любой может выдать себя за другого пользователя.",{"q":1353,"a":1354},"Когда обязательны Telegram Stars, а когда можно Stripe?","Цифровые товары и подписки внутри Mini App по правилам Apple и Google оплачиваются через Telegram Stars. Физические товары, услуги и консультации можно продавать через классические шлюзы вроде Stripe.","\u002Fimages\u002Fblog\u002Fblog_telegram_twa.jpg","ru",{},"\u002Fblog\u002Fru\u002Ftelegram-web-apps-sells",{"title":6,"description":1343},"blog\u002Fru\u002Ftelegram-web-apps-sells",[1362,1363,1364,1365,1366],"Telegram","Mini Apps","SaaS","Node.js","Frontend","YuRgxnAaTWzdgHfj4TSFOXTRw0OPRN5jjGwHaAdmD4k",{"blog":1369,"snippetGroups":1412},[1370,1372,1375,1378,1381,1384,1387,1389,1392,1395,1397,1400,1403,1406,1409],{"title":6,"url":1371},"\u002Fru\u002Fblog\u002Ftelegram-web-apps-sells",{"title":1373,"url":1374},"Generative Engine Optimization (GEO): Новое SEO для ChatGPT и ИИ-поиска","\u002Fru\u002Fblog\u002Fgenerative-engine-optimization-geo",{"title":1376,"url":1377},"Оптимизация сайта для ИИ-краулеров: Robots.txt и JSON-LD","\u002Fru\u002Fblog\u002Fwebsite-optimization-ai-crawlers",{"title":1379,"url":1380},"Сколько стоит разработка SaaS MVP в 2026 году: смета по этапам","\u002Fru\u002Fblog\u002Fsaas-mvp-development-cost",{"title":1382,"url":1383},"Как собрать MVP для B2B SaaS за 30 дней без технического долга","\u002Fru\u002Fblog\u002Fb2b-saas-mvp-30-days",{"title":1385,"url":1386},"Цена дешевого кода: Почему приходится переписывать SaaS MVP с нуля","\u002Fru\u002Fblog\u002Fsaas-development-cheap-code-trap",{"title":1388,"url":1284},"Монетизация SaaS: Как безболезненно настроить Stripe, подписки и биллинг",{"title":1390,"url":1391},"Архитектура SaaS: Single-Tenant против Multi-Tenant баз данных","\u002Fru\u002Fblog\u002Fsaas-multitenancy-databases",{"title":1393,"url":1394},"Чек-лист аудита кода: Как оценить техдолг перед масштабированием","\u002Fru\u002Fblog\u002Fcode-audit-technical-debt",{"title":1396,"url":1277},"ИИ-поддержка клиентов: Как собрать RAG-бота с бесшовной передачей менеджеру",{"title":1398,"url":1399},"Конструктор ботов или кастомная разработка: как выбрать в 2026 году","\u002Fru\u002Fblog\u002Fbot-builder-vs-custom-development",{"title":1401,"url":1402},"Сколько стоит разработка Telegram-бота в 2026 году: смета по типам ботов","\u002Fru\u002Fblog\u002Ftelegram-bot-development-cost",{"title":1404,"url":1405},"Что должно быть в ТЗ на Telegram-бота: чек-лист с примерами","\u002Fru\u002Fblog\u002Ftelegram-bot-specification-checklist",{"title":1407,"url":1408},"Почему вашей CRM нужен кастомный Telegram-бот (amoCRM & Bitrix24)","\u002Fru\u002Fblog\u002Ftelegram-bot-crm-integration",{"title":1410,"url":1411},"EzSwitch — утилита для быстрого переключения раскладки на macOS","\u002Fru\u002Fblog\u002Fezswitch-macos-switcher",{"CSS":1413,"JavaScript":1438,"Docker":1541,"PHP":1551,"Git":1604,"GitHub Actions":1617,"Go":1630,"Express.js":1651,"Next.js":1670,"Nginx":1686,"Prisma":1699,"React.js":1712,"Node.js":1731,"Tailwind CSS":1749,"TypeScript":1762,"Python":1778,"Vue":1794},[1414,1417,1420,1423,1426,1429,1432,1435],{"title":1415,"url":1416},"CSS Grid auto-fit — адаптивная сетка без media queries","\u002Fru\u002Fsnippets\u002Fcss-grid-auto-fit",{"title":1418,"url":1419},"Fluid Typography с clamp()","\u002Fru\u002Fsnippets\u002Fcss-fluid-typography",{"title":1421,"url":1422},"Skeleton Loading анимация (CSS)","\u002Fru\u002Fsnippets\u002Fcss-skeleton-loading",{"title":1424,"url":1425},"Градиентный текст (CSS)","\u002Fru\u002Fsnippets\u002Fcss-gradient-text",{"title":1427,"url":1428},"Кастомный scrollbar через CSS","\u002Fru\u002Fsnippets\u002Fcss-custom-scrollbar",{"title":1430,"url":1431},"Многострочный text-overflow с ellipsis","\u002Fru\u002Fsnippets\u002Fcss-line-clamp",{"title":1433,"url":1434},"Обрезание однострочного текста с многоточием","\u002Fru\u002Fsnippets\u002Fcss-truncate-text-ellipsis",{"title":1436,"url":1437},"Центрирование чего угодно через flexbox","\u002Fru\u002Fsnippets\u002Fcss-center-with-flexbox-css",[1439,1442,1445,1448,1451,1454,1457,1460,1463,1466,1469,1472,1475,1478,1481,1484,1487,1490,1493,1496,1499,1502,1505,1508,1511,1514,1517,1520,1523,1526,1529,1532,1535,1538],{"title":1440,"url":1441},"Date в\u002Fиз \"YYYY-MM-DD\" без сдвига часового пояса","\u002Fru\u002Fsnippets\u002Fjavascript-iso-date-without-timezone-shift",{"title":1443,"url":1444},"JSON.parse, который никогда не бросает исключение","\u002Fru\u002Fsnippets\u002Fjavascript-safe-json-parse",{"title":1446,"url":1447},"sleep() \u002F задержка в async\u002Fawait","\u002Fru\u002Fsnippets\u002Fjavascript-sleep-delay-async",{"title":1449,"url":1450},"Автоматическая раскладка инлайн-кнопок Telegram","\u002Fru\u002Fsnippets\u002Fjavascript-smart-telegram-button-layout",{"title":1452,"url":1453},"Автоматическое определение часового пояса посетителя","\u002Fru\u002Fsnippets\u002Fjavascript-detect-visitor-timezone",{"title":1455,"url":1456},"Безопасное объединение Tailwind-классов (хелпер cn)","\u002Fru\u002Fsnippets\u002Fjavascript-cn-classname-merge",{"title":1458,"url":1459},"Валидация email-адреса","\u002Fru\u002Fsnippets\u002Fjavascript-validate-email",{"title":1461,"url":1462},"Валидация медиа-URL","\u002Fru\u002Fsnippets\u002Fjavascript-validate-media-url",{"title":1464,"url":1465},"Выбор и исключение ключей объекта","\u002Fru\u002Fsnippets\u002Fjavascript-pick-omit-object-keys",{"title":1467,"url":1468},"Генерация UUID","\u002Fru\u002Fsnippets\u002Fjavascript-generate-uuid",{"title":1470,"url":1471},"Генерация slug из строки","\u002Fru\u002Fsnippets\u002Fjavascript-generate-slug-from-string",{"title":1473,"url":1474},"Глубокое клонирование объекта","\u002Fru\u002Fsnippets\u002Fjavascript-deep-clone-object",{"title":1476,"url":1477},"Дебаунс функции","\u002Fru\u002Fsnippets\u002Fjavascript-debounce-function",{"title":1479,"url":1480},"Декодировать payload JWT без верификации","\u002Fru\u002Fsnippets\u002Fjavascript-jwt-decode-payload",{"title":1482,"url":1483},"Заглавная первая буква строки","\u002Fru\u002Fsnippets\u002Fjavascript-capitalize-first-letter",{"title":1485,"url":1486},"Компактное форматирование чисел (1.2K, 3M)","\u002Fru\u002Fsnippets\u002Fjavascript-format-compact-number",{"title":1488,"url":1489},"Косинусное сходство между двумя векторами","\u002Fru\u002Fsnippets\u002Fjavascript-cosine-similarity",{"title":1491,"url":1492},"Обрезать текст по количеству слов","\u002Fru\u002Fsnippets\u002Fjavascript-truncate-text-by-words",{"title":1494,"url":1495},"Ответ на callback_query Telegram","\u002Fru\u002Fsnippets\u002Fjavascript-telegram-answer-callback-query",{"title":1497,"url":1498},"Отображение часового пояса со смещением GMT","\u002Fru\u002Fsnippets\u002Fjavascript-timezone-gmt-offset-label",{"title":1500,"url":1501},"Плавный скролл к якорю","\u002Fru\u002Fsnippets\u002Fjavascript-scroll-to-anchor",{"title":1503,"url":1504},"Получение геолокации по IP (ipapi.co)","\u002Fru\u002Fsnippets\u002Fjavascript-ip-geolocation-js",{"title":1506,"url":1507},"Получить ID и данные пользователя из Telegram-апдейта","\u002Fru\u002Fsnippets\u002Fjavascript-telegram-get-user-from-update",{"title":1509,"url":1510},"Преобразование произвольных значений в boolean","\u002Fru\u002Fsnippets\u002Fjavascript-to-boolean",{"title":1512,"url":1513},"Проверка, светлый цвет или тёмный (hex)","\u002Fru\u002Fsnippets\u002Fjavascript-is-color-light",{"title":1515,"url":1516},"Проверка, является ли цель события редактируемым полем","\u002Fru\u002Fsnippets\u002Fjavascript-is-editable-target",{"title":1518,"url":1519},"Разбор deep-link параметров Telegram \u002Fstart","\u002Fru\u002Fsnippets\u002Fjavascript-parse-telegram-start-params",{"title":1521,"url":1522},"Секунды до следующей смены суток\u002Fмесяца по UTC","\u002Fru\u002Fsnippets\u002Fjavascript-seconds-until-utc-rollover",{"title":1524,"url":1525},"Склонение слов через Intl.PluralRules","\u002Fru\u002Fsnippets\u002Fjavascript-pluralize-intl",{"title":1527,"url":1528},"Ссылка на пользователя Telegram и mention","\u002Fru\u002Fsnippets\u002Fjavascript-telegram-user-link",{"title":1530,"url":1531},"Троттлинг функции","\u002Fru\u002Fsnippets\u002Fjavascript-throttle-function",{"title":1533,"url":1534},"Удаление дублей из массива","\u002Fru\u002Fsnippets\u002Fjavascript-remove-duplicate-array-items",{"title":1536,"url":1537},"Управление уникальным отсортированным списком тегов","\u002Fru\u002Fsnippets\u002Fjavascript-dedupe-sort-tag-list",{"title":1539,"url":1540},"Форматирование даты\u002Fвремени в любом IANA-часовом поясе","\u002Fru\u002Fsnippets\u002Fjavascript-format-date-in-timezone",[1542,1545,1548],{"title":1543,"url":1544},"Docker Compose для Node.js + PostgreSQL + Redis","\u002Fru\u002Fsnippets\u002Fdocker-compose-node-postgres-redis",{"title":1546,"url":1547},"Docker multi-stage сборки для Node.js","\u002Fru\u002Fsnippets\u002Fdocker-multistage-build-nodejs",{"title":1549,"url":1550},"Docker безопасность — non-root пользователь и read-only","\u002Fru\u002Fsnippets\u002Fdocker-security-nonroot",[1552,1555,1558,1560,1563,1566,1569,1572,1575,1577,1580,1583,1586,1589,1592,1595,1598,1601],{"title":1553,"url":1554},"GET-запрос через cURL","\u002Fru\u002Fsnippets\u002Fphp-curl-get-request",{"title":1556,"url":1557},"Rate Limiting через Redis (PHP)","\u002Fru\u002Fsnippets\u002Fphp-rate-limiter-redis",{"title":1458,"url":1559},"\u002Fru\u002Fsnippets\u002Fphp-validate-email",{"title":1561,"url":1562},"Верификация Telegram Webhook Secret","\u002Fru\u002Fsnippets\u002Fphp-telegram-webhook-verify-secret",{"title":1564,"url":1565},"Генерация безопасного случайного пароля","\u002Fru\u002Fsnippets\u002Fphp-secure-random-password-php",{"title":1567,"url":1568},"Маскирование токена\u002FAPI-ключа для безопасного отображения","\u002Fru\u002Fsnippets\u002Fphp-mask-token-preview",{"title":1570,"url":1571},"Отправка сообщения через Telegram Bot API (PHP)","\u002Fru\u002Fsnippets\u002Fphp-telegram-send-message-php",{"title":1573,"url":1574},"Повтор операции с экспоненциальной задержкой и джиттером","\u002Fru\u002Fsnippets\u002Fphp-retry-with-backoff",{"title":1503,"url":1576},"\u002Fru\u002Fsnippets\u002Fphp-ip-geolocation-php",{"title":1578,"url":1579},"Получение реального IP клиента за прокси\u002FCDN","\u002Fru\u002Fsnippets\u002Fphp-get-client-ip-behind-proxy",{"title":1581,"url":1582},"Получение текущего GMT-смещения часового пояса в PHP","\u002Fru\u002Fsnippets\u002Fphp-timezone-gmt-offset",{"title":1584,"url":1585},"Получить ID пользователя Telegram через PHP-вебхук","\u002Fru\u002Fsnippets\u002Fphp-telegram-get-user-php",{"title":1587,"url":1588},"Приведение строки к валидному UTF-8","\u002Fru\u002Fsnippets\u002Fphp-sanitize-utf8-php",{"title":1590,"url":1591},"Разбор языкового тега BCP-47","\u002Fru\u002Fsnippets\u002Fphp-parse-bcp47-language-tag",{"title":1593,"url":1594},"Форматирование байтов в читаемый размер","\u002Fru\u002Fsnippets\u002Fphp-format-bytes-human-readable",{"title":1596,"url":1597},"Хеширование паролей bcrypt (PHP)","\u002Fru\u002Fsnippets\u002Fphp-hash-password-bcrypt-php",{"title":1599,"url":1600},"Цветной аватар с инициалами (HTML\u002FCSS)","\u002Fru\u002Fsnippets\u002Fphp-initials-avatar-php",{"title":1602,"url":1603},"Экранирование текста для Telegram MarkdownV2","\u002Fru\u002Fsnippets\u002Fphp-escape-telegram-markdownv2",[1605,1608,1611,1614],{"title":1606,"url":1607},"Git cherry-pick и bisect — точечная работа с коммитами","\u002Fru\u002Fsnippets\u002Fgit-cherry-pick-bisect",{"title":1609,"url":1610},"Git log — фильтрация и форматирование истории","\u002Fru\u002Fsnippets\u002Fgit-log-filter-format",{"title":1612,"url":1613},"Git squash коммитов и интерактивный rebase","\u002Fru\u002Fsnippets\u002Fgit-squash-rebase-workflow",{"title":1615,"url":1616},"Git stash — сохранение и восстановление черновиков","\u002Fru\u002Fsnippets\u002Fgit-stash-work-in-progress",[1618,1621,1624,1627],{"title":1619,"url":1620},"GitHub Actions — CI пайплайн для PR","\u002Fru\u002Fsnippets\u002Fgithub-actions-ci-pr",{"title":1622,"url":1623},"GitHub Actions — деплой Node.js на VPS через SSH","\u002Fru\u002Fsnippets\u002Fgithub-actions-deploy-vps",{"title":1625,"url":1626},"GitHub Actions — запланированные задачи по cron","\u002Fru\u002Fsnippets\u002Fgithub-actions-scheduled-cron",{"title":1628,"url":1629},"GitHub Actions — сборка и публикация Docker образа","\u002Fru\u002Fsnippets\u002Fgithub-actions-docker-build-push",[1631,1634,1637,1640,1642,1645,1648],{"title":1632,"url":1633},"HTTP GET-запрос","\u002Fru\u002Fsnippets\u002Fgo-http-get-request",{"title":1635,"url":1636},"HTTP Server с Graceful Shutdown (Go)","\u002Fru\u002Fsnippets\u002Fgo-graceful-shutdown",{"title":1638,"url":1639},"HTTP запрос с таймаутом через Context (Go)","\u002Fru\u002Fsnippets\u002Fgo-context-timeout",{"title":1467,"url":1641},"\u002Fru\u002Fsnippets\u002Fgo-generate-uuid",{"title":1643,"url":1644},"Параллельное выполнение задач через горутины","\u002Fru\u002Fsnippets\u002Fgo-goroutines-waitgroup",{"title":1646,"url":1647},"Построчное чтение файла","\u002Fru\u002Fsnippets\u002Fgo-read-file-lines-go",{"title":1649,"url":1650},"Создание простого HTTP-сервера","\u002Fru\u002Fsnippets\u002Fgo-simple-http-server",[1652,1655,1658,1661,1664,1667],{"title":1653,"url":1654},"JWT Auth Middleware (Express)","\u002Fru\u002Fsnippets\u002Fexpress.js-jwt-auth-middleware",{"title":1656,"url":1657},"JWT access и refresh token rotation в Express","\u002Fru\u002Fsnippets\u002Fexpress-jwt-refresh-tokens",{"title":1659,"url":1660},"Включение CORS в Express","\u002Fru\u002Fsnippets\u002Fexpress.js-cors-middleware",{"title":1662,"url":1663},"Ограничение частоты запросов","\u002Fru\u002Fsnippets\u002Fexpress.js-rate-limiting-middleware",{"title":1665,"url":1666},"Санитизация имени загруженного файла","\u002Fru\u002Fsnippets\u002Fexpress.js-sanitize-filename-express",{"title":1668,"url":1669},"Централизованный middleware обработки ошибок","\u002Fru\u002Fsnippets\u002Fexpress.js-error-handling-middleware",[1671,1674,1677,1680,1683],{"title":1672,"url":1673},"Next.js App Router middleware — аутентификация и редиректы","\u002Fru\u002Fsnippets\u002Fnextjs-middleware-auth",{"title":1675,"url":1676},"Генерация динамических метаданных страницы","\u002Fru\u002Fsnippets\u002Fnext.js-dynamic-metadata",{"title":1678,"url":1679},"Редирект из Server Component или Route Handler","\u002Fru\u002Fsnippets\u002Fnext.js-redirect",{"title":1681,"url":1682},"Создание обработчика API-роута","\u002Fru\u002Fsnippets\u002Fnext.js-api-route-handler",{"title":1684,"url":1685},"Чтение search params в серверном компоненте","\u002Fru\u002Fsnippets\u002Fnext.js-read-server-search-params-nextjs",[1687,1690,1693,1696],{"title":1688,"url":1689},"Nginx WebSocket прокси для real-time приложений","\u002Fru\u002Fsnippets\u002Fnginx-websocket-proxy",{"title":1691,"url":1692},"Nginx load balancing между Node.js инстансами","\u002Fru\u002Fsnippets\u002Fnginx-load-balancing-nodejs",{"title":1694,"url":1695},"Nginx reverse proxy с SSL termination","\u002Fru\u002Fsnippets\u002Fnginx-reverse-proxy-ssl",{"title":1697,"url":1698},"Nginx статический сервер с кешированием","\u002Fru\u002Fsnippets\u002Fnginx-static-cache-server",[1700,1703,1706,1709],{"title":1701,"url":1702},"Prisma CRUD — создание, чтение, обновление, удаление","\u002Fru\u002Fsnippets\u002Fprisma-crud-operations",{"title":1704,"url":1705},"Prisma миграции — создание, применение и откат","\u002Fru\u002Fsnippets\u002Fprisma-migrations-workflow",{"title":1707,"url":1708},"Prisma пагинация — cursor и offset","\u002Fru\u002Fsnippets\u002Fprisma-pagination-cursor-offset",{"title":1710,"url":1711},"Prisma схема — User, Post и связи","\u002Fru\u002Fsnippets\u002Fprisma-schema-user-relations",[1713,1716,1719,1722,1725,1728],{"title":1714,"url":1715},"React context + useReducer для глобального состояния","\u002Fru\u002Fsnippets\u002Freact-context-usereducer",{"title":1717,"url":1718},"React useMediaQuery — адаптивный хук","\u002Fru\u002Fsnippets\u002Freact-usemediaquery-hook",{"title":1720,"url":1721},"Хук useDebounce","\u002Fru\u002Fsnippets\u002Freact.js-use-debounce-hook",{"title":1723,"url":1724},"Хук useFetch","\u002Fru\u002Fsnippets\u002Freact.js-use-fetch-hook",{"title":1726,"url":1727},"Хук useOnClickOutside","\u002Fru\u002Fsnippets\u002Freact.js-use-click-outside-hook",{"title":1729,"url":1730},"Хук usePersistedState","\u002Fru\u002Fsnippets\u002Freact.js-use-local-storage-react",[1732,1735,1738,1740,1743,1746],{"title":1733,"url":1734},"Stripe webhook handler с идемпотентностью","\u002Fru\u002Fsnippets\u002Fstripe-webhook-idempotency",{"title":1736,"url":1737},"Асинхронное чтение файла","\u002Fru\u002Fsnippets\u002Fnode.js-read-file-async",{"title":1649,"url":1739},"\u002Fru\u002Fsnippets\u002Fnode.js-create-http-server",{"title":1741,"url":1742},"Сравнение строк за постоянное время для секретов","\u002Fru\u002Fsnippets\u002Fnode.js-timing-safe-string-compare",{"title":1744,"url":1745},"Хеширование и проверка пароля через bcrypt","\u002Fru\u002Fsnippets\u002Fnode.js-hash-password-bcrypt",{"title":1747,"url":1748},"Чтение переменных окружения со значением по умолчанию","\u002Fru\u002Fsnippets\u002Fnode.js-read-env-defaults-node",[1750,1753,1756,1759],{"title":1751,"url":1752},"Tailwind CSS dark mode со стратегией class","\u002Fru\u002Fsnippets\u002Ftailwind-dark-mode-class",{"title":1754,"url":1755},"Tailwind CSS адаптивная сетка карточек","\u002Fru\u002Fsnippets\u002Ftailwind-responsive-card-grid",{"title":1757,"url":1758},"Tailwind CSS анимации — keyframes и transition","\u002Fru\u002Fsnippets\u002Ftailwind-animations-keyframes",{"title":1760,"url":1761},"Tailwind CSS кастомная тема через CSS переменные","\u002Fru\u002Fsnippets\u002Ftailwind-custom-theme-variables",[1763,1766,1769,1772,1775],{"title":1764,"url":1765},"Zod схемы валидации для API","\u002Fru\u002Fsnippets\u002Fzod-api-validation-schema",{"title":1767,"url":1768},"useFetch с AbortController — TypeScript","\u002Fru\u002Fsnippets\u002Ftypescript-use-fetch-with-abort",{"title":1770,"url":1771},"Группировка массива по ключу","\u002Fru\u002Fsnippets\u002Ftypescript-group-by-key",{"title":1773,"url":1774},"Разбить массив на чанки","\u002Fru\u002Fsnippets\u002Ftypescript-chunk-array",{"title":1776,"url":1777},"Утилиты пагинации (offset\u002Flimit)","\u002Fru\u002Fsnippets\u002Ftypescript-pagination-offset-limit",[1779,1782,1785,1788,1791],{"title":1780,"url":1781},"asyncio.gather с ограничением concurrency","\u002Fru\u002Fsnippets\u002Fpython-async-gather-limited-python",{"title":1783,"url":1784},"Декоратор retry с backoff (Python)","\u002Fru\u002Fsnippets\u002Fpython-retry-decorator-python",{"title":1786,"url":1787},"Проверка существования файла или директории","\u002Fru\u002Fsnippets\u002Fpython-check-file-exists",{"title":1789,"url":1790},"Разворачивание вложенного списка","\u002Fru\u002Fsnippets\u002Fpython-flatten-nested-list-python",{"title":1792,"url":1793},"Чтение CSV-файла в список словарей","\u002Fru\u002Fsnippets\u002Fpython-read-csv-file",[1795,1798,1801,1804,1807,1810,1813,1816,1819],{"title":1796,"url":1797},"useDebouncedRef — отложенное реактивное значение","\u002Fru\u002Fsnippets\u002Fvue-use-debounced-ref",{"title":1799,"url":1800},"useIntersectionObserver — Vue composable","\u002Fru\u002Fsnippets\u002Fvue-use-intersection-observer",{"title":1802,"url":1803},"useMediaQuery — реактивный media query","\u002Fru\u002Fsnippets\u002Fvue-use-media-query",{"title":1805,"url":1806},"Хук useBreakpoints","\u002Fru\u002Fsnippets\u002Fvue-use-breakpoints",{"title":1808,"url":1809},"Хук useClipboardCopy","\u002Fru\u002Fsnippets\u002Fvue-use-clipboard-copy",{"title":1811,"url":1812},"Хук useDragAndDrop","\u002Fru\u002Fsnippets\u002Fvue-use-drag-and-drop",{"title":1814,"url":1815},"Хук usePlatform","\u002Fru\u002Fsnippets\u002Fvue-use-platform",{"title":1817,"url":1818},"Хук useTheme","\u002Fru\u002Fsnippets\u002Fvue-use-theme",{"title":1820,"url":1821},"Хук useViewMode","\u002Fru\u002Fsnippets\u002Fvue-use-view-mode",1784561414096]