[{"data":1,"prerenderedAt":11442},["ShallowReactive",2],{"header-latest":3,"all-posts":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",[1369,2366,2949,3564,3816,4443,4751,6094,6805,7483,8907,9207,9482,9754,11161],{"id":5,"title":6,"body":1370,"date":1342,"description":1343,"extension":1344,"faq":2359,"image":1355,"lang":1356,"meta":2363,"navigation":106,"path":1358,"published":106,"readTime":213,"seo":2364,"stem":1360,"tags":2365,"updated":1342,"__hash__":1367},{"type":8,"value":1371,"toc":2349},[1372,1376,1378,1380,1382,1384,1389,1393,1399,1403,1405,1995,1997,1999,2001,2005,2043,2045,2047,2281,2283,2289,2291,2315,2321,2325,2327,2339,2341,2347],[11,1373,13,1374,18],{},[15,1375,17],{},[11,1377,21],{},[11,1379,24],{},[26,1381,29],{"id":28},[11,1383,32],{},[34,1385,1387],{"className":1386,"code":38,"language":39,"meta":40},[37],[42,1388,38],{"__ignoreMap":40},[26,1390,47,1391],{"id":46},[42,1392,50],{},[11,1394,53,1395,56,1397,60],{},[42,1396,50],{},[42,1398,59],{},[11,1400,63,1401,67],{},[15,1402,66],{},[11,1404,70],{},[34,1406,1407],{"className":73,"code":74,"language":75,"meta":40,"style":40},[42,1408,1409,1421,1425,1433,1443,1453,1463,1473,1483,1493,1497,1501,1509,1519,1529,1533,1537,1569,1583,1601,1605,1615,1625,1629,1633,1637,1675,1679,1683,1693,1727,1743,1747,1751,1755,1765,1781,1789,1797,1801,1805,1815,1827,1835,1847,1851,1855,1869,1873,1883,1893,1897,1901,1905,1911,1929,1959,1969,1977,1987,1991],{"__ignoreMap":40},[79,1410,1411,1413,1415,1417,1419],{"class":81,"line":82},[79,1412,86],{"class":85},[79,1414,90],{"class":89},[79,1416,93],{"class":85},[79,1418,97],{"class":96},[79,1420,100],{"class":89},[79,1422,1423],{"class":81,"line":103},[79,1424,107],{"emptyLinePlaceholder":106},[79,1426,1427,1429,1431],{"class":81,"line":110},[79,1428,113],{"class":85},[79,1430,117],{"class":116},[79,1432,120],{"class":89},[79,1434,1435,1437,1439,1441],{"class":81,"line":123},[79,1436,127],{"class":126},[79,1438,130],{"class":85},[79,1440,134],{"class":133},[79,1442,100],{"class":89},[79,1444,1445,1447,1449,1451],{"class":81,"line":139},[79,1446,142],{"class":126},[79,1448,130],{"class":85},[79,1450,147],{"class":133},[79,1452,100],{"class":89},[79,1454,1455,1457,1459,1461],{"class":81,"line":152},[79,1456,155],{"class":126},[79,1458,158],{"class":85},[79,1460,147],{"class":133},[79,1462,100],{"class":89},[79,1464,1465,1467,1469,1471],{"class":81,"line":165},[79,1466,168],{"class":126},[79,1468,158],{"class":85},[79,1470,147],{"class":133},[79,1472,100],{"class":89},[79,1474,1475,1477,1479,1481],{"class":81,"line":177},[79,1476,180],{"class":126},[79,1478,158],{"class":85},[79,1480,147],{"class":133},[79,1482,100],{"class":89},[79,1484,1485,1487,1489,1491],{"class":81,"line":189},[79,1486,192],{"class":126},[79,1488,158],{"class":85},[79,1490,197],{"class":133},[79,1492,100],{"class":89},[79,1494,1495],{"class":81,"line":202},[79,1496,205],{"class":89},[79,1498,1499],{"class":81,"line":208},[79,1500,107],{"emptyLinePlaceholder":106},[79,1502,1503,1505,1507],{"class":81,"line":213},[79,1504,113],{"class":85},[79,1506,218],{"class":116},[79,1508,120],{"class":89},[79,1510,1511,1513,1515,1517],{"class":81,"line":223},[79,1512,226],{"class":126},[79,1514,130],{"class":85},[79,1516,197],{"class":133},[79,1518,100],{"class":89},[79,1520,1521,1523,1525,1527],{"class":81,"line":235},[79,1522,238],{"class":126},[79,1524,158],{"class":85},[79,1526,117],{"class":116},[79,1528,100],{"class":89},[79,1530,1531],{"class":81,"line":247},[79,1532,205],{"class":89},[79,1534,1535],{"class":81,"line":252},[79,1536,107],{"emptyLinePlaceholder":106},[79,1538,1539,1541,1543,1545,1547,1549,1551,1553,1555,1557,1559,1561,1563,1565,1567],{"class":81,"line":257},[79,1540,260],{"class":85},[79,1542,263],{"class":85},[79,1544,266],{"class":116},[79,1546,269],{"class":89},[79,1548,272],{"class":126},[79,1550,130],{"class":85},[79,1552,147],{"class":133},[79,1554,279],{"class":89},[79,1556,282],{"class":126},[79,1558,130],{"class":85},[79,1560,147],{"class":133},[79,1562,289],{"class":89},[79,1564,130],{"class":85},[79,1566,218],{"class":116},[79,1568,120],{"class":89},[79,1570,1571,1573,1575,1577,1579,1581],{"class":81,"line":298},[79,1572,301],{"class":85},[79,1574,304],{"class":133},[79,1576,307],{"class":85},[79,1578,310],{"class":85},[79,1580,313],{"class":116},[79,1582,316],{"class":89},[79,1584,1585,1587,1589,1591,1593,1595,1597,1599],{"class":81,"line":319},[79,1586,301],{"class":85},[79,1588,324],{"class":133},[79,1590,307],{"class":85},[79,1592,329],{"class":89},[79,1594,332],{"class":116},[79,1596,269],{"class":89},[79,1598,337],{"class":96},[79,1600,340],{"class":89},[79,1602,1603],{"class":81,"line":343},[79,1604,346],{"class":89},[79,1606,1607,1609,1611,1613],{"class":81,"line":349},[79,1608,352],{"class":85},[79,1610,355],{"class":89},[79,1612,358],{"class":85},[79,1614,361],{"class":89},[79,1616,1617,1619,1621,1623],{"class":81,"line":364},[79,1618,367],{"class":85},[79,1620,370],{"class":89},[79,1622,373],{"class":133},[79,1624,376],{"class":89},[79,1626,1627],{"class":81,"line":379},[79,1628,382],{"class":89},[79,1630,1631],{"class":81,"line":385},[79,1632,107],{"emptyLinePlaceholder":106},[79,1634,1635],{"class":81,"line":390},[79,1636,394],{"class":393},[79,1638,1639,1641,1643,1645,1647,1649,1651,1653,1655,1657,1659,1661,1663,1665,1667,1669,1671,1673],{"class":81,"line":397},[79,1640,301],{"class":85},[79,1642,402],{"class":133},[79,1644,307],{"class":85},[79,1646,407],{"class":89},[79,1648,93],{"class":116},[79,1650,412],{"class":89},[79,1652,415],{"class":116},[79,1654,418],{"class":89},[79,1656,421],{"class":116},[79,1658,269],{"class":89},[79,1660,426],{"class":126},[79,1662,429],{"class":85},[79,1664,432],{"class":89},[79,1666,435],{"class":85},[79,1668,438],{"class":96},[79,1670,441],{"class":89},[79,1672,444],{"class":116},[79,1674,447],{"class":89},[79,1676,1677],{"class":81,"line":450},[79,1678,346],{"class":89},[79,1680,1681],{"class":81,"line":455},[79,1682,458],{"class":393},[79,1684,1685,1687,1689,1691],{"class":81,"line":461},[79,1686,301],{"class":85},[79,1688,466],{"class":133},[79,1690,307],{"class":85},[79,1692,471],{"class":89},[79,1694,1695,1697,1699,1701,1703,1705,1707,1709,1711,1713,1715,1717,1719,1721,1723,1725],{"class":81,"line":474},[79,1696,477],{"class":89},[79,1698,480],{"class":116},[79,1700,269],{"class":89},[79,1702,426],{"class":126},[79,1704,429],{"class":85},[79,1706,489],{"class":96},[79,1708,426],{"class":89},[79,1710,494],{"class":96},[79,1712,497],{"class":89},[79,1714,60],{"class":96},[79,1716,332],{"class":116},[79,1718,269],{"class":96},[79,1720,426],{"class":89},[79,1722,289],{"class":96},[79,1724,510],{"class":96},[79,1726,513],{"class":89},[79,1728,1729,1731,1733,1735,1737,1739,1741],{"class":81,"line":516},[79,1730,477],{"class":89},[79,1732,521],{"class":116},[79,1734,269],{"class":89},[79,1736,526],{"class":96},[79,1738,529],{"class":133},[79,1740,526],{"class":96},[79,1742,340],{"class":89},[79,1744,1745],{"class":81,"line":536},[79,1746,107],{"emptyLinePlaceholder":106},[79,1748,1749],{"class":81,"line":541},[79,1750,544],{"class":393},[79,1752,1753],{"class":81,"line":547},[79,1754,550],{"class":393},[79,1756,1757,1759,1761,1763],{"class":81,"line":553},[79,1758,301],{"class":85},[79,1760,558],{"class":133},[79,1762,307],{"class":85},[79,1764,563],{"class":89},[79,1766,1767,1769,1771,1773,1775,1777,1779],{"class":81,"line":566},[79,1768,477],{"class":89},[79,1770,571],{"class":116},[79,1772,269],{"class":89},[79,1774,576],{"class":96},[79,1776,279],{"class":89},[79,1778,581],{"class":96},[79,1780,513],{"class":89},[79,1782,1783,1785,1787],{"class":81,"line":586},[79,1784,477],{"class":89},[79,1786,591],{"class":116},[79,1788,594],{"class":89},[79,1790,1791,1793,1795],{"class":81,"line":597},[79,1792,477],{"class":89},[79,1794,602],{"class":116},[79,1796,447],{"class":89},[79,1798,1799],{"class":81,"line":607},[79,1800,107],{"emptyLinePlaceholder":106},[79,1802,1803],{"class":81,"line":612},[79,1804,615],{"class":393},[79,1806,1807,1809,1811,1813],{"class":81,"line":618},[79,1808,301],{"class":85},[79,1810,623],{"class":133},[79,1812,307],{"class":85},[79,1814,563],{"class":89},[79,1816,1817,1819,1821,1823,1825],{"class":81,"line":630},[79,1818,477],{"class":89},[79,1820,571],{"class":116},[79,1822,269],{"class":89},[79,1824,576],{"class":96},[79,1826,641],{"class":89},[79,1828,1829,1831,1833],{"class":81,"line":644},[79,1830,477],{"class":89},[79,1832,591],{"class":116},[79,1834,651],{"class":89},[79,1836,1837,1839,1841,1843,1845],{"class":81,"line":654},[79,1838,477],{"class":89},[79,1840,602],{"class":116},[79,1842,269],{"class":89},[79,1844,663],{"class":96},[79,1846,340],{"class":89},[79,1848,1849],{"class":81,"line":668},[79,1850,107],{"emptyLinePlaceholder":106},[79,1852,1853],{"class":81,"line":673},[79,1854,676],{"class":393},[79,1856,1857,1859,1861,1863,1865,1867],{"class":81,"line":679},[79,1858,301],{"class":85},[79,1860,684],{"class":133},[79,1862,307],{"class":85},[79,1864,689],{"class":89},[79,1866,692],{"class":85},[79,1868,695],{"class":89},[79,1870,1871],{"class":81,"line":698},[79,1872,107],{"emptyLinePlaceholder":106},[79,1874,1875,1877,1879,1881],{"class":81,"line":703},[79,1876,352],{"class":85},[79,1878,355],{"class":89},[79,1880,358],{"class":85},[79,1882,712],{"class":89},[79,1884,1885,1887,1889,1891],{"class":81,"line":715},[79,1886,367],{"class":85},[79,1888,370],{"class":89},[79,1890,373],{"class":133},[79,1892,376],{"class":89},[79,1894,1895],{"class":81,"line":726},[79,1896,382],{"class":89},[79,1898,1899],{"class":81,"line":731},[79,1900,107],{"emptyLinePlaceholder":106},[79,1902,1903],{"class":81,"line":736},[79,1904,739],{"class":393},[79,1906,1907,1909],{"class":81,"line":742},[79,1908,745],{"class":85},[79,1910,120],{"class":89},[79,1912,1913,1915,1917,1919,1921,1923,1925,1927],{"class":81,"line":750},[79,1914,753],{"class":85},[79,1916,756],{"class":133},[79,1918,307],{"class":85},[79,1920,329],{"class":89},[79,1922,332],{"class":116},[79,1924,269],{"class":89},[79,1926,767],{"class":96},[79,1928,340],{"class":89},[79,1930,1931,1933,1935,1937,1939,1941,1943,1945,1947,1949,1951,1953,1955,1957],{"class":81,"line":772},[79,1932,753],{"class":85},[79,1934,777],{"class":133},[79,1936,130],{"class":85},[79,1938,117],{"class":116},[79,1940,307],{"class":85},[79,1942,786],{"class":89},[79,1944,789],{"class":85},[79,1946,792],{"class":133},[79,1948,60],{"class":89},[79,1950,797],{"class":116},[79,1952,800],{"class":89},[79,1954,130],{"class":85},[79,1956,805],{"class":133},[79,1958,100],{"class":89},[79,1960,1961,1963,1965,1967],{"class":81,"line":810},[79,1962,367],{"class":85},[79,1964,370],{"class":89},[79,1966,817],{"class":133},[79,1968,820],{"class":89},[79,1970,1971,1973,1975],{"class":81,"line":823},[79,1972,826],{"class":89},[79,1974,829],{"class":85},[79,1976,832],{"class":89},[79,1978,1979,1981,1983,1985],{"class":81,"line":835},[79,1980,367],{"class":85},[79,1982,370],{"class":89},[79,1984,817],{"class":133},[79,1986,376],{"class":89},[79,1988,1989],{"class":81,"line":846},[79,1990,382],{"class":89},[79,1992,1993],{"class":81,"line":851},[79,1994,205],{"class":89},[26,1996,857],{"id":856},[11,1998,860],{},[862,2000,865],{"id":864},[11,2002,868,2003,130],{},[42,2004,871],{},[34,2006,2007],{"className":73,"code":874,"language":75,"meta":40,"style":40},[42,2008,2009,2013,2019,2023,2035,2039],{"__ignoreMap":40},[79,2010,2011],{"class":81,"line":82},[79,2012,881],{"class":393},[79,2014,2015,2017],{"class":81,"line":103},[79,2016,871],{"class":116},[79,2018,888],{"class":89},[79,2020,2021],{"class":81,"line":110},[79,2022,893],{"class":89},[79,2024,2025,2027,2029,2031,2033],{"class":81,"line":123},[79,2026,898],{"class":89},[79,2028,901],{"class":96},[79,2030,904],{"class":89},[79,2032,817],{"class":133},[79,2034,909],{"class":89},[79,2036,2037],{"class":81,"line":139},[79,2038,914],{"class":89},[79,2040,2041],{"class":81,"line":152},[79,2042,919],{"class":89},[862,2044,923],{"id":922},[11,2046,926],{},[34,2048,2049],{"className":73,"code":929,"language":75,"meta":40,"style":40},[42,2050,2051,2055,2067,2071,2081,2097,2117,2121,2131,2147,2153,2161,2171,2175,2183,2193,2197,2201,2211,2223,2235,2247,2259,2263,2267,2271,2277],{"__ignoreMap":40},[79,2052,2053],{"class":81,"line":82},[79,2054,936],{"class":393},[79,2056,2057,2059,2061,2063,2065],{"class":81,"line":103},[79,2058,86],{"class":85},[79,2060,943],{"class":89},[79,2062,93],{"class":85},[79,2064,948],{"class":96},[79,2066,100],{"class":89},[79,2068,2069],{"class":81,"line":110},[79,2070,107],{"emptyLinePlaceholder":106},[79,2072,2073,2075,2077,2079],{"class":81,"line":123},[79,2074,260],{"class":85},[79,2076,263],{"class":85},[79,2078,963],{"class":116},[79,2080,966],{"class":89},[79,2082,2083,2085,2087,2089,2091,2093,2095],{"class":81,"line":139},[79,2084,301],{"class":85},[79,2086,973],{"class":133},[79,2088,307],{"class":85},[79,2090,978],{"class":116},[79,2092,269],{"class":89},[79,2094,373],{"class":133},[79,2096,340],{"class":89},[79,2098,2099,2101,2103,2105,2107,2109,2111,2113,2115],{"class":81,"line":152},[79,2100,301],{"class":85},[79,2102,777],{"class":133},[79,2104,307],{"class":85},[79,2106,978],{"class":116},[79,2108,997],{"class":89},[79,2110,1000],{"class":133},[79,2112,1003],{"class":89},[79,2114,1006],{"class":133},[79,2116,340],{"class":89},[79,2118,2119],{"class":81,"line":165},[79,2120,107],{"emptyLinePlaceholder":106},[79,2122,2123,2125,2127,2129],{"class":81,"line":177},[79,2124,1017],{"class":116},[79,2126,1020],{"class":89},[79,2128,1023],{"class":85},[79,2130,120],{"class":89},[79,2132,2133,2135,2137,2139,2141,2143,2145],{"class":81,"line":189},[79,2134,753],{"class":85},[79,2136,1032],{"class":133},[79,2138,307],{"class":85},[79,2140,1037],{"class":89},[79,2142,1040],{"class":85},[79,2144,1043],{"class":133},[79,2146,1046],{"class":89},[79,2148,2149,2151],{"class":81,"line":202},[79,2150,1051],{"class":85},[79,2152,1054],{"class":89},[79,2154,2155,2157,2159],{"class":81,"line":208},[79,2156,1059],{"class":89},[79,2158,1062],{"class":116},[79,2160,447],{"class":89},[79,2162,2163,2165,2167,2169],{"class":81,"line":213},[79,2164,1059],{"class":89},[79,2166,1071],{"class":116},[79,2168,1074],{"class":89},[79,2170,1077],{"class":393},[79,2172,2173],{"class":81,"line":223},[79,2174,1082],{"class":89},[79,2176,2177,2179,2181],{"class":81,"line":235},[79,2178,1087],{"class":89},[79,2180,1090],{"class":85},[79,2182,1093],{"class":89},[79,2184,2185,2187,2189,2191],{"class":81,"line":247},[79,2186,1098],{"class":89},[79,2188,1090],{"class":85},[79,2190,1103],{"class":133},[79,2192,100],{"class":89},[79,2194,2195],{"class":81,"line":252},[79,2196,107],{"emptyLinePlaceholder":106},[79,2198,2199],{"class":81,"line":257},[79,2200,1114],{"class":393},[79,2202,2203,2205,2207,2209],{"class":81,"line":298},[79,2204,1119],{"class":85},[79,2206,1122],{"class":133},[79,2208,307],{"class":85},[79,2210,1127],{"class":89},[79,2212,2213,2215,2217,2219,2221],{"class":81,"line":319},[79,2214,1132],{"class":89},[79,2216,1135],{"class":116},[79,2218,269],{"class":89},[79,2220,1140],{"class":96},[79,2222,1143],{"class":89},[79,2224,2225,2227,2229,2231,2233],{"class":81,"line":343},[79,2226,1132],{"class":89},[79,2228,1135],{"class":116},[79,2230,269],{"class":89},[79,2232,1154],{"class":96},[79,2234,1157],{"class":89},[79,2236,2237,2239,2241,2243,2245],{"class":81,"line":349},[79,2238,1132],{"class":89},[79,2240,1135],{"class":116},[79,2242,269],{"class":89},[79,2244,1168],{"class":96},[79,2246,1171],{"class":89},[79,2248,2249,2251,2253,2255,2257],{"class":81,"line":364},[79,2250,1132],{"class":89},[79,2252,1135],{"class":116},[79,2254,269],{"class":89},[79,2256,1182],{"class":96},[79,2258,1185],{"class":89},[79,2260,2261],{"class":81,"line":379},[79,2262,1190],{"class":89},[79,2264,2265],{"class":81,"line":385},[79,2266,1195],{"class":89},[79,2268,2269],{"class":81,"line":390},[79,2270,107],{"emptyLinePlaceholder":106},[79,2272,2273,2275],{"class":81,"line":397},[79,2274,1204],{"class":85},[79,2276,1207],{"class":89},[79,2278,2279],{"class":81,"line":450},[79,2280,205],{"class":89},[26,2282,1215],{"id":1214},[11,2284,1218,2285,355,2287,441],{},[15,2286,1221],{},[42,2288,1224],{},[11,2290,1227],{},[1229,2292,2293,2297,2305,2309],{},[1232,2294,2295,1237],{},[15,2296,1236],{},[1232,2298,2299,1243,2301,1247,2303,60],{},[15,2300,1242],{},[42,2302,1246],{},[42,2304,1224],{},[1232,2306,2307,1255],{},[15,2308,1254],{},[1232,2310,2311,1261,2313,1265],{},[15,2312,1260],{},[42,2314,1264],{},[11,2316,1268,2317,1274,2319,60],{},[1270,2318,1273],{"href":1272},[1270,2320,1278],{"href":1277},[11,2322,1281,2323,1286],{},[1270,2324,1285],{"href":1284},[26,2326,1290],{"id":1289},[1292,2328,2329,2334],{},[1232,2330,2331,1302],{},[1270,2332,1301],{"href":1298,"rel":2333},[1300],[1232,2335,2336,1310],{},[1270,2337,1309],{"href":1307,"rel":2338},[1300],[11,2340,1313],{},[11,2342,1316,2343,1321,2345,1326],{},[1270,2344,1320],{"href":1319},[1270,2346,1325],{"href":1324},[1328,2348,1330],{},{"title":40,"searchDepth":103,"depth":103,"links":2350},[2351,2352,2353,2357,2358],{"id":28,"depth":103,"text":29},{"id":46,"depth":103,"text":1335},{"id":856,"depth":103,"text":857,"children":2354},[2355,2356],{"id":864,"depth":110,"text":865},{"id":922,"depth":110,"text":923},{"id":1214,"depth":103,"text":1215},{"id":1289,"depth":103,"text":1290},[2360,2361,2362],{"q":1347,"a":1348},{"q":1350,"a":1351},{"q":1353,"a":1354},{},{"title":6,"description":1343},[1362,1363,1364,1365,1366],{"id":2367,"title":2368,"body":2369,"date":2926,"description":2927,"extension":1344,"faq":2928,"image":2938,"lang":1356,"meta":2939,"navigation":106,"path":2940,"published":106,"readTime":213,"seo":2941,"stem":2942,"tags":2943,"updated":1342,"__hash__":2948},"blog\u002Fblog\u002Fru\u002Fgenerative-engine-optimization-geo.md","Generative Engine Optimization (GEO): Новое SEO для ChatGPT и ИИ-поиска",{"type":8,"value":2370,"toc":2915},[2371,2374,2377,2383,2386,2390,2393,2399,2402,2406,2414,2516,2519,2523,2527,2530,2544,2548,2551,2565,2569,2572,2576,2579,2854,2865,2872,2874,2900,2903,2912],[11,2372,2373],{},"Поисковая экосистема интернета переживает самую масштабную революцию с момента появления Google в 1998 году. Традиционная поисковая оптимизация (SEO) строилась вокруг ранжирования ссылок на странице выдачи (SERP). Однако сегодня миллионы пользователей игнорируют поисковую выдачу. Они обращаются напрямую к ИИ-ассистентам — ChatGPT Search, Perplexity, Gemini и Claude — для получения синтезированных ответов.",[11,2375,2376],{},"Если вашего продукта, SaaS-сервиса или студии нет в ответах ИИ-ассистентов с соответствующими ссылками-цитатами, ваш бизнес становится невидимым для огромного пласта платежеспособной аудитории.",[11,2378,2379,2380,60],{},"Чтобы оставаться конкурентоспособными, компании должны оптимизировать свои ресурсы под требования ИИ-краулеров. Это направление называется ",[15,2381,2382],{},"GEO (Generative Engine Optimization)",[11,2384,2385],{},"В этой статье мы подробно разберем работу генеративных поисковых систем, изучим научные факторы ранжирования и внедрим технические решения для попадания в рекомендации ИИ.",[26,2387,2389],{"id":2388},"архитектура-работы-ии-поиска-rag-пайплайн","Архитектура работы ИИ-поиска (RAG-пайплайн)",[11,2391,2392],{},"В отличие от классических поисковиков, индексирующих ключевые слова, ИИ-платформы используют гибридный пайплайн извлечения данных (RAG):",[34,2394,2397],{"className":2395,"code":2396,"language":39,"meta":40},[37],"[Запрос: \"Лучшая multi-tenant CRM для недвижимости\"]\n                             │\n                             v\n           (Понимание запроса и веб-поиск)\n   ИИ переформулирует запрос -> Ищет в Bing\u002FGoogle API\n                             │\n                             v\n             (Парсинг и эмбеддинг топ-10 страниц)\n     ИИ-краулеры считывают HTML -> Векторизуют текст\n                             │\n                             v\n            (RAG-ранжирование и синтез ответа)\n     Сопоставляет вектор запроса с векторами страниц ->\nВыбирает топ-3 источника -> Синтезирует ответ с ссылками\n                             │\n                             v\n[Ответ: \"Я рекомендую AcmeCRM [1], так как у нее есть изоляция Postgres схем [2]\"]\n",[42,2398,2396],{"__ignoreMap":40},[11,2400,2401],{},"Чтобы попасть в этот ответ, ваш сайт должен не просто находиться высоко в традиционном поиске, но и содержать информацию в форме, которую реранкер ИИ сочтет авторитетной, лаконичной и легко компилируемой.",[26,2403,2405],{"id":2404},"_1-факторы-ранжирования-geo-данные-исследований","1. Факторы ранжирования GEO (Данные исследований)",[11,2407,2408,2409,2413],{},"В совместном исследовании ученых из Принстона, Технологического института Джорджии и Института ИИ Аллена (ученая работа ",[2410,2411,2412],"em",{},"“Generative Engine Optimization”",") были протестированы различные методы изменения контента и оценено их влияние на частоту цитирования ИИ-моделями:",[2415,2416,2417,2434],"table",{},[2418,2419,2420],"thead",{},[2421,2422,2423,2428,2431],"tr",{},[2424,2425,2427],"th",{"align":2426},"left","GEO-стратегия",[2424,2429,2430],{"align":2426},"Суть метода",[2424,2432,2433],{"align":2426},"Рост цитирования в ответах ИИ",[2435,2436,2437,2454,2470,2486,2501],"tbody",{},[2421,2438,2439,2446,2449],{},[2440,2441,2442,2445],"td",{"align":2426},[15,2443,2444],{},"Cite Sources"," (Авторитетные ссылки)",[2440,2447,2448],{"align":2426},"Добавление исходящих ссылок на научные работы, гос. ресурсы или крупные СМИ.",[2440,2450,2451],{"align":2426},[15,2452,2453],{},"+40.3%",[2421,2455,2456,2462,2465],{},[2440,2457,2458,2461],{"align":2426},[15,2459,2460],{},"Include Statistics"," (Точные данные)",[2440,2463,2464],{"align":2426},"Добавление в текст конкретных цифр, процентов и статистических метрик.",[2440,2466,2467],{"align":2426},[15,2468,2469],{},"+37.5%",[2421,2471,2472,2478,2481],{},[2440,2473,2474,2477],{"align":2426},[15,2475,2476],{},"Information Gain"," (Уникальность)",[2440,2479,2480],{"align":2426},"Наличие фактов или выводов, которых нет у других конкурентов из топ-10.",[2440,2482,2483],{"align":2426},[15,2484,2485],{},"+32.1%",[2421,2487,2488,2493,2496],{},[2440,2489,2490],{"align":2426},[15,2491,2492],{},"Q&A блоки",[2440,2494,2495],{"align":2426},"Форматирование заголовков в виде вопросов с кратким ответом сразу после них.",[2440,2497,2498],{"align":2426},[15,2499,2500],{},"+25.8%",[2421,2502,2503,2508,2511],{},[2440,2504,2505],{"align":2426},[15,2506,2507],{},"Таблицы и списки",[2440,2509,2510],{"align":2426},"Структурирование сложных данных в таблицы Markdown.",[2440,2512,2513],{"align":2426},[15,2514,2515],{},"+18.2%",[11,2517,2518],{},"Классический SEO-копирайтинг (перенасыщение ключевыми словами, общие рекламные фразы) снижает вероятность попадания в ИИ-ответы, так как ухудшает читаемость и усложняет суммаризацию текста.",[26,2520,2522],{"id":2521},"_2-практический-чеклист-по-geo-оптимизации","2. Практический чеклист по GEO-оптимизации",[862,2524,2526],{"id":2525},"а-используйте-заголовки-в-формате-qa-вопрос-ответ","А. Используйте заголовки в формате Q&A (Вопрос-Ответ)",[11,2528,2529],{},"Нейросети сканируют текст в поисках готовых ответов.",[1292,2531,2532,2538],{},[1232,2533,2534,2537],{},[2410,2535,2536],{},"Неправильно",": «Наши интеграции платежных систем».",[1232,2539,2540,2543],{},[2410,2541,2542],{},"Правильно",": «Как наш сервис интегрирует Stripe подписки?» Сразу после этого заголовка дайте лаконичный ответ в 2-3 предложениях, и только затем описывайте детали.",[862,2545,2547],{"id":2546},"б-ссылайтесь-на-внешние-источники","Б. Ссылайтесь на внешние источники",[11,2549,2550],{},"Подтверждайте свои слова ссылками на авторитетные базы.",[1292,2552,2553],{},[1232,2554,2555,2558,2559,2564],{},[2410,2556,2557],{},"Пример",": «Согласно стандартам ",[1270,2560,2563],{"href":2561,"rel":2562},"https:\u002F\u002Fowasp.org\u002Fwww-project-top-ten\u002F",[1300],"безопасности OWASP Top 10",", очистка пользовательского ввода является ключевым...»",[862,2566,2568],{"id":2567},"в-оптимизируйте-читаемость-контента","В. Оптимизируйте читаемость контента",[11,2570,2571],{},"Используйте простой и понятный язык, активный залог и структурированные списки. Избегайте сложных деепричастных оборотов и рекламной «воды».",[26,2573,2575],{"id":2574},"_3-передача-структурированных-данных-через-json-ld","3. Передача структурированных данных через JSON-LD",[11,2577,2578],{},"Микроразметка позволяет ИИ-ботам безошибочно распознавать информацию о вашем бренде. Вот пример структуры JSON-LD для страницы ИТ-консалтинга, которая внедряется в шапку сайта:",[34,2580,2584],{"className":2581,"code":2582,"language":2583,"meta":40,"style":40},"language-json shiki shiki-themes github-dark","{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"ConsultingService\",\n  \"@id\": \"https:\u002F\u002Falexrel.com\u002Fru\u002Fconsultations#service\",\n  \"name\": \"Технический аудит и архитектурные консультации\",\n  \"description\": \"Независимый аудит кодовой базы, проектирование баз данных и планирование SaaS MVP.\",\n  \"provider\": {\n    \"@type\": \"Person\",\n    \"name\": \"Алекс Рел\",\n    \"jobTitle\": \"Senior Fullstack Developer\",\n    \"url\": \"https:\u002F\u002Falexrel.com\"\n  },\n  \"offers\": {\n    \"@type\": \"Offer\",\n    \"priceCurrency\": \"USD\",\n    \"price\": \"250\",\n    \"priceSpecification\": {\n      \"@type\": \"UnitPriceSpecification\",\n      \"priceType\": \"https:\u002F\u002Fschema.org\u002FRecurringPrice\",\n      \"billingIncrement\": 1,\n      \"referenceQuantity\": {\n        \"@type\": \"QuantitativeValue\",\n        \"value\": 1,\n        \"unitCode\": \"HUR\"\n      }\n    }\n  }\n}\n","json",[42,2585,2586,2591,2605,2617,2629,2641,2653,2661,2673,2685,2697,2707,2712,2719,2730,2742,2754,2761,2773,2785,2797,2804,2816,2827,2837,2842,2846,2850],{"__ignoreMap":40},[79,2587,2588],{"class":81,"line":82},[79,2589,2590],{"class":89},"{\n",[79,2592,2593,2596,2599,2602],{"class":81,"line":103},[79,2594,2595],{"class":133},"  \"@context\"",[79,2597,2598],{"class":89},": ",[79,2600,2601],{"class":96},"\"https:\u002F\u002Fschema.org\"",[79,2603,2604],{"class":89},",\n",[79,2606,2607,2610,2612,2615],{"class":81,"line":110},[79,2608,2609],{"class":133},"  \"@type\"",[79,2611,2598],{"class":89},[79,2613,2614],{"class":96},"\"ConsultingService\"",[79,2616,2604],{"class":89},[79,2618,2619,2622,2624,2627],{"class":81,"line":123},[79,2620,2621],{"class":133},"  \"@id\"",[79,2623,2598],{"class":89},[79,2625,2626],{"class":96},"\"https:\u002F\u002Falexrel.com\u002Fru\u002Fconsultations#service\"",[79,2628,2604],{"class":89},[79,2630,2631,2634,2636,2639],{"class":81,"line":139},[79,2632,2633],{"class":133},"  \"name\"",[79,2635,2598],{"class":89},[79,2637,2638],{"class":96},"\"Технический аудит и архитектурные консультации\"",[79,2640,2604],{"class":89},[79,2642,2643,2646,2648,2651],{"class":81,"line":152},[79,2644,2645],{"class":133},"  \"description\"",[79,2647,2598],{"class":89},[79,2649,2650],{"class":96},"\"Независимый аудит кодовой базы, проектирование баз данных и планирование SaaS MVP.\"",[79,2652,2604],{"class":89},[79,2654,2655,2658],{"class":81,"line":165},[79,2656,2657],{"class":133},"  \"provider\"",[79,2659,2660],{"class":89},": {\n",[79,2662,2663,2666,2668,2671],{"class":81,"line":177},[79,2664,2665],{"class":133},"    \"@type\"",[79,2667,2598],{"class":89},[79,2669,2670],{"class":96},"\"Person\"",[79,2672,2604],{"class":89},[79,2674,2675,2678,2680,2683],{"class":81,"line":189},[79,2676,2677],{"class":133},"    \"name\"",[79,2679,2598],{"class":89},[79,2681,2682],{"class":96},"\"Алекс Рел\"",[79,2684,2604],{"class":89},[79,2686,2687,2690,2692,2695],{"class":81,"line":202},[79,2688,2689],{"class":133},"    \"jobTitle\"",[79,2691,2598],{"class":89},[79,2693,2694],{"class":96},"\"Senior Fullstack Developer\"",[79,2696,2604],{"class":89},[79,2698,2699,2702,2704],{"class":81,"line":208},[79,2700,2701],{"class":133},"    \"url\"",[79,2703,2598],{"class":89},[79,2705,2706],{"class":96},"\"https:\u002F\u002Falexrel.com\"\n",[79,2708,2709],{"class":81,"line":213},[79,2710,2711],{"class":89},"  },\n",[79,2713,2714,2717],{"class":81,"line":223},[79,2715,2716],{"class":133},"  \"offers\"",[79,2718,2660],{"class":89},[79,2720,2721,2723,2725,2728],{"class":81,"line":235},[79,2722,2665],{"class":133},[79,2724,2598],{"class":89},[79,2726,2727],{"class":96},"\"Offer\"",[79,2729,2604],{"class":89},[79,2731,2732,2735,2737,2740],{"class":81,"line":247},[79,2733,2734],{"class":133},"    \"priceCurrency\"",[79,2736,2598],{"class":89},[79,2738,2739],{"class":96},"\"USD\"",[79,2741,2604],{"class":89},[79,2743,2744,2747,2749,2752],{"class":81,"line":252},[79,2745,2746],{"class":133},"    \"price\"",[79,2748,2598],{"class":89},[79,2750,2751],{"class":96},"\"250\"",[79,2753,2604],{"class":89},[79,2755,2756,2759],{"class":81,"line":257},[79,2757,2758],{"class":133},"    \"priceSpecification\"",[79,2760,2660],{"class":89},[79,2762,2763,2766,2768,2771],{"class":81,"line":298},[79,2764,2765],{"class":133},"      \"@type\"",[79,2767,2598],{"class":89},[79,2769,2770],{"class":96},"\"UnitPriceSpecification\"",[79,2772,2604],{"class":89},[79,2774,2775,2778,2780,2783],{"class":81,"line":319},[79,2776,2777],{"class":133},"      \"priceType\"",[79,2779,2598],{"class":89},[79,2781,2782],{"class":96},"\"https:\u002F\u002Fschema.org\u002FRecurringPrice\"",[79,2784,2604],{"class":89},[79,2786,2787,2790,2792,2795],{"class":81,"line":343},[79,2788,2789],{"class":133},"      \"billingIncrement\"",[79,2791,2598],{"class":89},[79,2793,2794],{"class":133},"1",[79,2796,2604],{"class":89},[79,2798,2799,2802],{"class":81,"line":349},[79,2800,2801],{"class":133},"      \"referenceQuantity\"",[79,2803,2660],{"class":89},[79,2805,2806,2809,2811,2814],{"class":81,"line":364},[79,2807,2808],{"class":133},"        \"@type\"",[79,2810,2598],{"class":89},[79,2812,2813],{"class":96},"\"QuantitativeValue\"",[79,2815,2604],{"class":89},[79,2817,2818,2821,2823,2825],{"class":81,"line":379},[79,2819,2820],{"class":133},"        \"value\"",[79,2822,2598],{"class":89},[79,2824,2794],{"class":133},[79,2826,2604],{"class":89},[79,2828,2829,2832,2834],{"class":81,"line":385},[79,2830,2831],{"class":133},"        \"unitCode\"",[79,2833,2598],{"class":89},[79,2835,2836],{"class":96},"\"HUR\"\n",[79,2838,2839],{"class":81,"line":390},[79,2840,2841],{"class":89},"      }\n",[79,2843,2844],{"class":81,"line":397},[79,2845,1190],{"class":89},[79,2847,2848],{"class":81,"line":450},[79,2849,382],{"class":89},[79,2851,2852],{"class":81,"line":455},[79,2853,205],{"class":89},[11,2855,2856,2857,2859,2860,2864],{},"При оптимизации моих личных продуктов, таких как платформа ",[1270,2858,1273],{"href":1272}," и сервис изучения языков ",[1270,2861,2863],{"href":2862},"\u002Fru\u002Fprojects\u002Flingvohabit","LingvoHabit",", я применил именно эти архитектурные правила. Сегодня оба проекта регулярно рекомендуются и цитируются в ответах ChatGPT Search и Perplexity.",[11,2866,2867,2868,60],{},"О том, как правильно настроить файлы robots.txt и структуру разметки для получения этих результатов, читайте в моем техническом руководстве ",[1270,2869,2871],{"href":2870},"\u002Fru\u002Fblog\u002Fwebsite-optimization-ai-crawlers","Оптимизация сайта для ИИ-краулеров: Robots.txt и JSON-LD",[26,2873,1290],{"id":1289},[1292,2875,2876,2884,2892],{},[1232,2877,2878,2883],{},[1270,2879,2882],{"href":2880,"rel":2881},"https:\u002F\u002Farxiv.org\u002Fabs\u002F2311.09735",[1300],"GEO: Generative Engine Optimization"," — оригинальное исследование Princeton \u002F Georgia Tech \u002F Allen AI с данными по всем стратегиям из таблицы выше",[1232,2885,2886,2891],{},[1270,2887,2890],{"href":2888,"rel":2889},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs",[1300],"Google Search Central"," — официальная документация по индексации и структурированным данным",[1232,2893,2894,2899],{},[1270,2895,2898],{"href":2896,"rel":2897},"https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fbots",[1300],"Документация OpenAI по краулерам"," — как GPTBot и OAI-SearchBot сканируют сайты",[11,2901,2902],{},"Адаптация структуры и контента вашего сайта под требования ИИ-поисковиков — это единственный способ сохранить лидерство по объему органического трафика в эпоху генеративного веб-поиска.",[11,2904,2905,2906,1321,2910,60],{},"Если вы хотите внедрить GEO на своем проекте, провести аудит видимости бренда в ChatGPT\u002FPerplexity или настроить техническое SEO для ИИ-поисковиков, ознакомьтесь с моей услугой ",[1270,2907,2909],{"href":2908},"\u002Fru\u002Fgeo-seo","GEO & SEO продвижение под ключ",[1270,2911,1325],{"href":1324},[1328,2913,2914],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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":2916},[2917,2918,2919,2924,2925],{"id":2388,"depth":103,"text":2389},{"id":2404,"depth":103,"text":2405},{"id":2521,"depth":103,"text":2522,"children":2920},[2921,2922,2923],{"id":2525,"depth":110,"text":2526},{"id":2546,"depth":110,"text":2547},{"id":2567,"depth":110,"text":2568},{"id":2574,"depth":103,"text":2575},{"id":1289,"depth":103,"text":1290},"2026-07-02","Детальное руководство по оптимизации сайтов под требования ИИ-ассистентов (GEO). Разбираем алгоритмы ранжирования в ChatGPT Search, Perplexity и Gemini.",[2929,2932,2935],{"q":2930,"a":2931},"Что такое GEO простыми словами?","GEO (Generative Engine Optimization) — оптимизация сайта под ИИ-поисковики, чтобы ChatGPT, Perplexity и Gemini находили, цитировали и рекомендовали ваш бренд в своих ответах. Если SEO борется за позицию в выдаче, то GEO — за упоминание внутри сгенерированного ответа.",{"q":2933,"a":2934},"Заменяет ли GEO классическое SEO?","Нет, они дополняют друг друга. ИИ-поисковики опираются на индексы классических систем (ChatGPT Search использует Bing, Gemini — Google), поэтому без базовой индексируемости GEO не работает. SEO приводит трафик из выдачи, GEO — упоминания и переходы из ответов ИИ.",{"q":2936,"a":2937},"Какие приёмы GEO дают наибольший эффект?","По данным исследования Princeton \u002F Georgia Tech \u002F Allen AI: ссылки на авторитетные источники (+40%), конкретная статистика в тексте (+37%), уникальные факты, которых нет у конкурентов (+32%), и заголовки в формате вопрос-ответ (+26%).","\u002Fimages\u002Fblog\u002Fblog_geo_seo.jpg",{},"\u002Fblog\u002Fru\u002Fgenerative-engine-optimization-geo",{"title":2368,"description":2927},"blog\u002Fru\u002Fgenerative-engine-optimization-geo",[2944,2945,2946,2947],"GEO","SEO","AI","Маркетинг","c1qSQwVrhPD5X8LnCsQncmqvKgR4tZ7Xpc_I7LW33Hk",{"id":2950,"title":2871,"body":2951,"date":3543,"description":3544,"extension":1344,"faq":3545,"image":3555,"lang":1356,"meta":3556,"navigation":106,"path":3557,"published":106,"readTime":213,"seo":3558,"stem":3559,"tags":3560,"updated":1342,"__hash__":3563},"blog\u002Fblog\u002Fru\u002Fwebsite-optimization-ai-crawlers.md",{"type":8,"value":2952,"toc":3535},[2953,2972,2975,2986,2990,2993,2999,3005,3009,3012,3019,3025,3340,3344,3347,3354,3358,3379,3472,3475,3483,3485,3517,3524,3532],[11,2954,2955,2956,2959,2960,2963,2964,2967,2968,2971],{},"Сфера веб-краулинга изменилась навсегда. На сервера больше не заходят только привычные Googlebot, YandexBot или Bingbot. Сегодня логи веб-серверов переполнены запросами от ",[15,2957,2958],{},"GPTBot"," (OpenAI), ",[15,2961,2962],{},"ClaudeBot"," (Anthropic), ",[15,2965,2966],{},"PerplexityBot"," и ",[15,2969,2970],{},"OAI-SearchBot"," (ИИ-поиск от OpenAI).",[11,2973,2974],{},"Поскольку эти модели сканируют сеть в реальном времени, чтобы отвечать на вопросы пользователей, техническая инфраструктура вашего сайта должна быть готова к приему таких ботов: разрешать индексацию контента, передавать точные семантические данные и закрывать доступ к приватным API.",[11,2976,2977,2978,2981,2982,2985],{},"В этой статье мы настроим файл ",[42,2979,2980],{},"robots.txt"," для ИИ-агентов, разработаем связанную структуру микроразметки ",[42,2983,2984],{},"JSON-LD"," и разберем проблемы рендеринга JS при парсинге.",[26,2987,2989],{"id":2988},"_1-конфигурация-robotstxt-для-ии-краулеров","1. Конфигурация Robots.txt для ИИ-краулеров",[11,2991,2992],{},"ИИ-боты должны быть выделены в отдельные директивы. Вы хотите открыть им доступ к статьям блога и страницам услуг для получения рекомендаций, но обязаны заблокировать сканирование личных кабинетов, админки и роутов авторизации.",[11,2994,2995,2996,2998],{},"Пример файла ",[42,2997,2980],{}," для продакшена:",[34,3000,3003],{"className":3001,"code":3002,"language":39,"meta":40},[37],"# Общие правила для стандартных поисковиков\nUser-agent: *\nAllow: \u002F\nDisallow: \u002Fadmin\u002F\nDisallow: \u002Fapi\u002F\nDisallow: \u002Fcheckout\u002F\n\n# Правила для OpenAI ChatGPT (сбор материалов для ответов)\nUser-agent: GPTBot\nAllow: \u002F\nDisallow: \u002Fadmin\u002F\nDisallow: \u002Fapi\u002F\nDisallow: \u002Fcheckout\u002F\nDisallow: \u002Fprivate\u002F\n\n# Правила для OpenAI Search (поиск в реальном времени)\nUser-agent: OAI-SearchBot\nAllow: \u002F\nDisallow: \u002Fadmin\u002F\nDisallow: \u002Fapi\u002F\n\n# Правила для Anthropic Claude\nUser-agent: ClaudeBot\nAllow: \u002F\nDisallow: \u002Fadmin\u002F\nDisallow: \u002Fapi\u002F\n\n# Правила для Perplexity Search\nUser-agent: PerplexityBot\nAllow: \u002F\nDisallow: \u002Fadmin\u002F\nDisallow: \u002Fapi\u002F\n\n# Блокируем агрессивных ботов, которые обучаются на данных без отдачи трафика\nUser-agent: CCBot\nDisallow: \u002F\nUser-agent: Amazonbot\nDisallow: \u002F\n",[42,3004,3002],{"__ignoreMap":40},[26,3006,3008],{"id":3007},"_2-разработка-связанных-графов-json-ld-schema","2. Разработка связанных графов JSON-LD Schema",[11,3010,3011],{},"Большие языковые модели считывают информацию не так, как люди — они строят связи между именованными сущностями. Если вы опишете цены и услуги обычным текстом, ИИ может ошибиться при интерпретации.",[11,3013,3014,3015,3018],{},"Внедрение структурированного ",[15,3016,3017],{},"JSON-LD (JSON Linked Data)"," в шапку страниц гарантирует точную передачу фактов.",[11,3020,3021,3022,130],{},"Пример связанного графа для организации и оказываемой ею ",[15,3023,3024],{},"услуги разработки SaaS",[34,3026,3028],{"className":2581,"code":3027,"language":2583,"meta":40,"style":40},"{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@graph\": [\n    {\n      \"@type\": \"Organization\",\n      \"@id\": \"https:\u002F\u002Falexrel.com\u002F#organization\",\n      \"name\": \"Разработка Алекса Рела\",\n      \"url\": \"https:\u002F\u002Falexrel.com\",\n      \"logo\": \"https:\u002F\u002Falexrel.com\u002Flogo.png\",\n      \"sameAs\": [\n        \"https:\u002F\u002Fgithub.com\u002Falexrel\",\n        \"https:\u002F\u002Flinkedin.com\u002Fin\u002Falexrel\"\n      ]\n    },\n    {\n      \"@type\": \"Service\",\n      \"@id\": \"https:\u002F\u002Falexrel.com\u002Fru\u002Fsaas-development#service\",\n      \"name\": \"Архитектура и разработка B2B SaaS под ключ\",\n      \"description\": \"Разработка многопользовательских платформ от структуры БД до биллинга Stripe и RAG-ботов поддержки.\",\n      \"provider\": {\n        \"@id\": \"https:\u002F\u002Falexrel.com\u002F#organization\"\n      },\n      \"areaServed\": \"Worldwide\",\n      \"offers\": {\n        \"@type\": \"Offer\",\n        \"priceCurrency\": \"USD\",\n        \"price\": \"5000\",\n        \"priceSpecification\": {\n          \"@type\": \"UnitPriceSpecification\",\n          \"priceType\": \"https:\u002F\u002Fschema.org\u002FMinimumPrice\",\n          \"price\": \"5000\",\n          \"priceCurrency\": \"USD\"\n        }\n      }\n    }\n  ]\n}\n",[42,3029,3030,3034,3044,3052,3057,3068,3080,3092,3104,3116,3123,3130,3135,3140,3145,3149,3160,3171,3182,3194,3201,3211,3216,3228,3235,3245,3256,3268,3275,3286,3298,3309,3319,3324,3328,3332,3336],{"__ignoreMap":40},[79,3031,3032],{"class":81,"line":82},[79,3033,2590],{"class":89},[79,3035,3036,3038,3040,3042],{"class":81,"line":103},[79,3037,2595],{"class":133},[79,3039,2598],{"class":89},[79,3041,2601],{"class":96},[79,3043,2604],{"class":89},[79,3045,3046,3049],{"class":81,"line":110},[79,3047,3048],{"class":133},"  \"@graph\"",[79,3050,3051],{"class":89},": [\n",[79,3053,3054],{"class":81,"line":123},[79,3055,3056],{"class":89},"    {\n",[79,3058,3059,3061,3063,3066],{"class":81,"line":139},[79,3060,2765],{"class":133},[79,3062,2598],{"class":89},[79,3064,3065],{"class":96},"\"Organization\"",[79,3067,2604],{"class":89},[79,3069,3070,3073,3075,3078],{"class":81,"line":152},[79,3071,3072],{"class":133},"      \"@id\"",[79,3074,2598],{"class":89},[79,3076,3077],{"class":96},"\"https:\u002F\u002Falexrel.com\u002F#organization\"",[79,3079,2604],{"class":89},[79,3081,3082,3085,3087,3090],{"class":81,"line":165},[79,3083,3084],{"class":133},"      \"name\"",[79,3086,2598],{"class":89},[79,3088,3089],{"class":96},"\"Разработка Алекса Рела\"",[79,3091,2604],{"class":89},[79,3093,3094,3097,3099,3102],{"class":81,"line":177},[79,3095,3096],{"class":133},"      \"url\"",[79,3098,2598],{"class":89},[79,3100,3101],{"class":96},"\"https:\u002F\u002Falexrel.com\"",[79,3103,2604],{"class":89},[79,3105,3106,3109,3111,3114],{"class":81,"line":189},[79,3107,3108],{"class":133},"      \"logo\"",[79,3110,2598],{"class":89},[79,3112,3113],{"class":96},"\"https:\u002F\u002Falexrel.com\u002Flogo.png\"",[79,3115,2604],{"class":89},[79,3117,3118,3121],{"class":81,"line":202},[79,3119,3120],{"class":133},"      \"sameAs\"",[79,3122,3051],{"class":89},[79,3124,3125,3128],{"class":81,"line":208},[79,3126,3127],{"class":96},"        \"https:\u002F\u002Fgithub.com\u002Falexrel\"",[79,3129,2604],{"class":89},[79,3131,3132],{"class":81,"line":213},[79,3133,3134],{"class":96},"        \"https:\u002F\u002Flinkedin.com\u002Fin\u002Falexrel\"\n",[79,3136,3137],{"class":81,"line":223},[79,3138,3139],{"class":89},"      ]\n",[79,3141,3142],{"class":81,"line":235},[79,3143,3144],{"class":89},"    },\n",[79,3146,3147],{"class":81,"line":247},[79,3148,3056],{"class":89},[79,3150,3151,3153,3155,3158],{"class":81,"line":252},[79,3152,2765],{"class":133},[79,3154,2598],{"class":89},[79,3156,3157],{"class":96},"\"Service\"",[79,3159,2604],{"class":89},[79,3161,3162,3164,3166,3169],{"class":81,"line":257},[79,3163,3072],{"class":133},[79,3165,2598],{"class":89},[79,3167,3168],{"class":96},"\"https:\u002F\u002Falexrel.com\u002Fru\u002Fsaas-development#service\"",[79,3170,2604],{"class":89},[79,3172,3173,3175,3177,3180],{"class":81,"line":298},[79,3174,3084],{"class":133},[79,3176,2598],{"class":89},[79,3178,3179],{"class":96},"\"Архитектура и разработка B2B SaaS под ключ\"",[79,3181,2604],{"class":89},[79,3183,3184,3187,3189,3192],{"class":81,"line":319},[79,3185,3186],{"class":133},"      \"description\"",[79,3188,2598],{"class":89},[79,3190,3191],{"class":96},"\"Разработка многопользовательских платформ от структуры БД до биллинга Stripe и RAG-ботов поддержки.\"",[79,3193,2604],{"class":89},[79,3195,3196,3199],{"class":81,"line":343},[79,3197,3198],{"class":133},"      \"provider\"",[79,3200,2660],{"class":89},[79,3202,3203,3206,3208],{"class":81,"line":349},[79,3204,3205],{"class":133},"        \"@id\"",[79,3207,2598],{"class":89},[79,3209,3210],{"class":96},"\"https:\u002F\u002Falexrel.com\u002F#organization\"\n",[79,3212,3213],{"class":81,"line":364},[79,3214,3215],{"class":89},"      },\n",[79,3217,3218,3221,3223,3226],{"class":81,"line":379},[79,3219,3220],{"class":133},"      \"areaServed\"",[79,3222,2598],{"class":89},[79,3224,3225],{"class":96},"\"Worldwide\"",[79,3227,2604],{"class":89},[79,3229,3230,3233],{"class":81,"line":385},[79,3231,3232],{"class":133},"      \"offers\"",[79,3234,2660],{"class":89},[79,3236,3237,3239,3241,3243],{"class":81,"line":390},[79,3238,2808],{"class":133},[79,3240,2598],{"class":89},[79,3242,2727],{"class":96},[79,3244,2604],{"class":89},[79,3246,3247,3250,3252,3254],{"class":81,"line":397},[79,3248,3249],{"class":133},"        \"priceCurrency\"",[79,3251,2598],{"class":89},[79,3253,2739],{"class":96},[79,3255,2604],{"class":89},[79,3257,3258,3261,3263,3266],{"class":81,"line":450},[79,3259,3260],{"class":133},"        \"price\"",[79,3262,2598],{"class":89},[79,3264,3265],{"class":96},"\"5000\"",[79,3267,2604],{"class":89},[79,3269,3270,3273],{"class":81,"line":455},[79,3271,3272],{"class":133},"        \"priceSpecification\"",[79,3274,2660],{"class":89},[79,3276,3277,3280,3282,3284],{"class":81,"line":461},[79,3278,3279],{"class":133},"          \"@type\"",[79,3281,2598],{"class":89},[79,3283,2770],{"class":96},[79,3285,2604],{"class":89},[79,3287,3288,3291,3293,3296],{"class":81,"line":474},[79,3289,3290],{"class":133},"          \"priceType\"",[79,3292,2598],{"class":89},[79,3294,3295],{"class":96},"\"https:\u002F\u002Fschema.org\u002FMinimumPrice\"",[79,3297,2604],{"class":89},[79,3299,3300,3303,3305,3307],{"class":81,"line":516},[79,3301,3302],{"class":133},"          \"price\"",[79,3304,2598],{"class":89},[79,3306,3265],{"class":96},[79,3308,2604],{"class":89},[79,3310,3311,3314,3316],{"class":81,"line":536},[79,3312,3313],{"class":133},"          \"priceCurrency\"",[79,3315,2598],{"class":89},[79,3317,3318],{"class":96},"\"USD\"\n",[79,3320,3321],{"class":81,"line":541},[79,3322,3323],{"class":89},"        }\n",[79,3325,3326],{"class":81,"line":547},[79,3327,2841],{"class":89},[79,3329,3330],{"class":81,"line":553},[79,3331,1190],{"class":89},[79,3333,3334],{"class":81,"line":566},[79,3335,914],{"class":89},[79,3337,3338],{"class":81,"line":586},[79,3339,205],{"class":89},[26,3341,3343],{"id":3342},"_3-решение-проблем-клиентского-рендеринга-csr-для-ии","3. Решение проблем клиентского рендеринга (CSR) для ИИ",[11,3345,3346],{},"Классический поисковик Googlebot выделяет гигантские мощности для рендеринга JavaScript. Однако большинство ИИ-краулеров оптимизируют скорость и скачивают только чистый, необработанный HTML-код.",[11,3348,3349,3350,3353],{},"Если ваше приложение использует Client-Side Rendering (например, собранный React или Vue проект без бэкенда с пустым ",[42,3351,3352],{},"\u003Cdiv id=\"app\">\u003C\u002Fdiv>","), ИИ увидит пустую страницу и не сможет прочитать информацию о вас.",[862,3355,3357],{"id":3356},"решение-для-проектов-на-nuxt-3","Решение для проектов на Nuxt 3:",[1229,3359,3360,3373],{},[1232,3361,3362,3365,3366,3369,3370,60],{},[15,3363,3364],{},"Включите Server-Side Rendering (SSR)",": Убедитесь, что параметр ",[42,3367,3368],{},"ssr: true"," активирован в ",[42,3371,3372],{},"nuxt.config.ts",[1232,3374,3375,3378],{},[15,3376,3377],{},"Настройте пререндеринг для статических страниц",": Сгенерируйте HTML-файлы заранее на этапе сборки:",[34,3380,3382],{"className":73,"code":3381,"language":75,"meta":40,"style":40},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  routeRules: {\n    '\u002F': { prerender: true },\n    '\u002Fsaas-development': { prerender: true },\n    '\u002Ftelegram-bots': { prerender: true },\n    '\u002Fgeo-seo': { prerender: true },\n    '\u002Fconsultations': { prerender: true }\n  }\n});\n",[42,3383,3384,3389,3401,3406,3419,3430,3441,3452,3463,3467],{"__ignoreMap":40},[79,3385,3386],{"class":81,"line":82},[79,3387,3388],{"class":393},"\u002F\u002F nuxt.config.ts\n",[79,3390,3391,3393,3396,3399],{"class":81,"line":103},[79,3392,260],{"class":85},[79,3394,3395],{"class":85}," default",[79,3397,3398],{"class":116}," defineNuxtConfig",[79,3400,888],{"class":89},[79,3402,3403],{"class":81,"line":110},[79,3404,3405],{"class":89},"  routeRules: {\n",[79,3407,3408,3411,3414,3416],{"class":81,"line":123},[79,3409,3410],{"class":96},"    '\u002F'",[79,3412,3413],{"class":89},": { prerender: ",[79,3415,817],{"class":133},[79,3417,3418],{"class":89}," },\n",[79,3420,3421,3424,3426,3428],{"class":81,"line":139},[79,3422,3423],{"class":96},"    '\u002Fsaas-development'",[79,3425,3413],{"class":89},[79,3427,817],{"class":133},[79,3429,3418],{"class":89},[79,3431,3432,3435,3437,3439],{"class":81,"line":152},[79,3433,3434],{"class":96},"    '\u002Ftelegram-bots'",[79,3436,3413],{"class":89},[79,3438,817],{"class":133},[79,3440,3418],{"class":89},[79,3442,3443,3446,3448,3450],{"class":81,"line":165},[79,3444,3445],{"class":96},"    '\u002Fgeo-seo'",[79,3447,3413],{"class":89},[79,3449,817],{"class":133},[79,3451,3418],{"class":89},[79,3453,3454,3457,3459,3461],{"class":81,"line":177},[79,3455,3456],{"class":96},"    '\u002Fconsultations'",[79,3458,3413],{"class":89},[79,3460,817],{"class":133},[79,3462,909],{"class":89},[79,3464,3465],{"class":81,"line":189},[79,3466,382],{"class":89},[79,3468,3469],{"class":81,"line":202},[79,3470,3471],{"class":89},"});\n",[11,3473,3474],{},"Это гарантирует, что при обращении GPTBot или ClaudeBot к вашему серверу они мгновенно получат наполненный текстом HTML-документ.",[11,3476,3477,3478,2967,3480,3482],{},"При проектировании инфраструктуры индексации для проектов ",[1270,3479,2863],{"href":2862},[1270,3481,1273],{"href":1272}," мы внедрили именно такие схемы пререндеринга и вложенной разметки. Это позволило нам добиться быстрого сканирования и цитирования страниц в ИИ-поисковиках.",[26,3484,1290],{"id":1289},[1292,3486,3487,3493,3501,3509],{},[1232,3488,3489,3492],{},[1270,3490,2898],{"href":2896,"rel":3491},[1300]," — актуальные user-agent'ы GPTBot и OAI-SearchBot",[1232,3494,3495,3500],{},[1270,3496,3499],{"href":3497,"rel":3498},"https:\u002F\u002Fsupport.anthropic.com\u002Fen\u002Farticles\u002F8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler",[1300],"Anthropic: как работает ClaudeBot"," — официальные правила краулера Claude",[1232,3502,3503,3508],{},[1270,3504,3507],{"href":3505,"rel":3506},"https:\u002F\u002Fschema.org",[1300],"Schema.org"," — полный словарь типов структурированных данных",[1232,3510,3511,3516],{},[1270,3512,3515],{"href":3513,"rel":3514},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fcrawling-indexing\u002Frobots\u002Fintro",[1300],"Google: введение в robots.txt"," — синтаксис и приоритеты директив",[11,3518,3519,3520,60],{},"Подготовка технического слоя для ИИ-сканирования — залог сохранения позиций вашего бизнеса на рынке поискового трафика нового поколения. Подробнее о том, как устроен этот маркетинг будущего, читайте в моей статье ",[1270,3521,3523],{"href":3522},"\u002Fru\u002Fblog\u002Fgenerative-engine-optimization-geo","Generative Engine Optimization (GEO) — Новое SEO",[11,3525,3526,3527,1321,3530,60],{},"Если вам нужна профессиональная помощь в проектировании JSON-LD микроразметки, настройке SSR-роутинга или конфигурации robots.txt под ИИ-ботов, изучите мою страницу ",[1270,3528,3529],{"href":2908},"GEO & SEO продвижения под ключ",[1270,3531,1325],{"href":1324},[1328,3533,3534],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":40,"searchDepth":103,"depth":103,"links":3536},[3537,3538,3539,3542],{"id":2988,"depth":103,"text":2989},{"id":3007,"depth":103,"text":3008},{"id":3342,"depth":103,"text":3343,"children":3540},[3541],{"id":3356,"depth":110,"text":3357},{"id":1289,"depth":103,"text":1290},"2026-06-28","Детальное техническое руководство по настройке Robots.txt для ИИ-ботов, проектированию связанных графов разметки JSON-LD и решению проблем с рендерингом JavaScript.",[3546,3549,3552],{"q":3547,"a":3548},"Нужно ли пускать на сайт всех ИИ-ботов подряд?","Нет. Разделяйте ботов по назначению: поисковые и отвечающие боты (GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot) стоит пускать — они приводят цитирования и трафик. Чисто обучающие краулеры вроде CCBot можно блокировать: они собирают данные, не отдавая ничего взамен.",{"q":3550,"a":3551},"Увидят ли ИИ-краулеры контент моего SPA на Vue или React?","Не гарантированно. Часть ИИ-ботов не исполняет JavaScript, поэтому клиентский рендеринг для них — пустая страница. Решение — SSR или пререндеринг: сервер должен отдавать готовый HTML с текстом и разметкой.",{"q":3553,"a":3554},"Какая JSON-LD разметка важнее всего для ИИ-поиска?","Связанный граф @graph с Organization\u002FPerson (кто вы), Service или Product (что предлагаете) и WebPage с dateModified. Критично, чтобы разметка точно совпадала с видимым контентом — расхождение подрывает доверие к источнику.","\u002Fimages\u002Fblog\u002Fblog_ai_crawlers.jpg",{},"\u002Fblog\u002Fru\u002Fwebsite-optimization-ai-crawlers",{"title":2871,"description":3544},"blog\u002Fru\u002Fwebsite-optimization-ai-crawlers",[2944,2945,2984,3561,3562],"Schema","Техническое","U47986nUG5WVUtzpocg8dpuksAwU0Kzaq2OCZdyHMjY",{"id":3565,"title":3566,"body":3567,"date":3791,"description":3792,"extension":1344,"faq":3793,"image":3806,"lang":1356,"meta":3807,"navigation":106,"path":3808,"published":106,"readTime":165,"seo":3809,"stem":3810,"tags":3811,"updated":1342,"__hash__":3815},"blog\u002Fblog\u002Fru\u002Fsaas-mvp-development-cost.md","Сколько стоит разработка SaaS MVP в 2026 году: смета по этапам",{"type":8,"value":3568,"toc":3785},[3569,3572,3576,3579,3648,3655,3659,3662,3696,3700,3703,3734,3755,3759,3766,3773],[11,3570,3571],{},"Разработка SaaS MVP под ключ стоит от $4 000 и занимает 4–6 недель — за эти деньги вы получаете работающий продукт с регистрацией, личными кабинетами, приёмом подписок и панелью администратора. Полноценная платформа с интеграциями обходится дороже и строится за 2–4 месяца. Ниже — из каких этапов складывается смета, где прячутся расходы после запуска и почему «дешёвый MVP за $1 000» обычно оказывается самым дорогим решением.",[26,3573,3575],{"id":3574},"из-каких-этапов-складывается-цена","Из каких этапов складывается цена?",[11,3577,3578],{},"Мой процесс разработки SaaS состоит из четырёх этапов, и каждый — отдельная строка сметы:",[2415,3580,3581,3594],{},[2418,3582,3583],{},[2421,3584,3585,3588,3591],{},[2424,3586,3587],{"align":2426},"Этап",[2424,3589,3590],{"align":2426},"Что происходит",[2424,3592,3593],{"align":2426},"Доля бюджета",[2435,3595,3596,3609,3622,3635],{},[2421,3597,3598,3603,3606],{},[2440,3599,3600],{"align":2426},[15,3601,3602],{},"1. Анализ и ТЗ",[2440,3604,3605],{"align":2426},"Проектирование архитектуры, схема базы данных, выбор стека, детальная спецификация",[2440,3607,3608],{"align":2426},"~10–15%",[2421,3610,3611,3616,3619],{},[2440,3612,3613],{"align":2426},[15,3614,3615],{},"2. MVP",[2440,3617,3618],{"align":2426},"Регистрация, личные кабинеты, ядро продукта, биллинг через Stripe",[2440,3620,3621],{"align":2426},"~50%",[2421,3623,3624,3629,3632],{},[2440,3625,3626],{"align":2426},[15,3627,3628],{},"3. Масштабирование",[2440,3630,3631],{"align":2426},"Интеграции с внешними API, командные аккаунты, тарифные планы",[2440,3633,3634],{"align":2426},"~25%",[2421,3636,3637,3642,3645],{},[2440,3638,3639],{"align":2426},[15,3640,3641],{},"4. Запуск",[2440,3643,3644],{"align":2426},"Деплой на сервер, бэкапы, мониторинг, финальное тестирование",[2440,3646,3647],{"align":2426},"~10%",[11,3649,3650,3651,3654],{},"Ключевая мысль: половина бюджета уходит на ",[15,3652,3653],{},"ядро продукта"," — ту функцию, за которую клиенты будут платить. Если подрядчик тратит половину сметы на «красивый лендинг и анимации», у продукта проблемы с приоритетами.",[26,3656,3658],{"id":3657},"почему-нельзя-просто-собрать-на-конструкторе","Почему нельзя просто «собрать на конструкторе»?",[11,3660,3661],{},"Можно — и иногда нужно: для проверки спроса без уникальной логики no-code честно экономит месяцы. Кастомная разработка становится оправданной, когда появляется хотя бы одно из:",[1292,3663,3664,3670,3680,3690],{},[1232,3665,3666,3669],{},[15,3667,3668],{},"уникальная бизнес-логика"," — алгоритмы, расчёты, автоматизации, которых нет в готовых блоках;",[1232,3671,3672,3675,3676,3679],{},[15,3673,3674],{},"подписки и сложный биллинг"," — апгрейды, льготные периоды, вебхуки платёжных систем (как это устроено технически, я разбирал в статье ",[1270,3677,3678],{"href":1284},"про интеграцию Stripe",");",[1232,3681,3682,3685,3686,3679],{},[15,3683,3684],{},"требования к данным"," — изоляция клиентов, экспорт, соответствие регуляторике (подробнее — в разборе ",[1270,3687,3689],{"href":3688},"\u002Fru\u002Fblog\u002Fsaas-multitenancy-databases","multi-tenant архитектур",[1232,3691,3692,3695],{},[15,3693,3694],{},"план масштабирования"," — конструкторы упираются в потолок, и миграция с них стоит дороже, чем изначальная разработка.",[26,3697,3699],{"id":3698},"где-прячутся-расходы-после-запуска","Где прячутся расходы после запуска?",[11,3701,3702],{},"Смета разработки — не вся стоимость владения продуктом. Заложите в финансовую модель:",[1292,3704,3705,3711,3722,3728],{},[1232,3706,3707,3710],{},[15,3708,3709],{},"Инфраструктура"," — VPS или облако, от десятков долларов в месяц на старте; растёт вместе с нагрузкой.",[1232,3712,3713,3716,3717,441],{},[15,3714,3715],{},"Платёжные комиссии"," — Stripe берёт процент с каждой транзакции (",[1270,3718,3721],{"href":3719,"rel":3720},"https:\u002F\u002Fstripe.com\u002Fpricing",[1300],"тарифы Stripe",[1232,3723,3724,3727],{},[15,3725,3726],{},"Внешние API"," — почтовые сервисы, ИИ-модели, аналитика: оплата по использованию.",[1232,3729,3730,3733],{},[15,3731,3732],{},"Развитие"," — SaaS без обновлений умирает; закладывайте бюджет на итерации после первых отзывов пользователей.",[11,3735,3736,3737,355,3740,3745,3746,355,3749,3754],{},"Рынок при этом растёт: мировые расходы на SaaS прогнозируются на уровне ",[15,3738,3739],{},"$300 млрд",[1270,3741,3744],{"href":3742,"rel":3743},"https:\u002F\u002Fwww.gartner.com\u002Fen\u002Fnewsroom\u002Fpress-releases\u002F2024-08-28-gartner-predicts-75-percent-of-enterprises-will-prioritize-backup-of-saas-applications-as-a-critical-requirement-by-2028",[1300],"Gartner, 2024","), а публичные SaaS-компании оцениваются в медианные ",[15,3747,3748],{},"7.0x годовой выручки",[1270,3750,3753],{"href":3751,"rel":3752},"https:\u002F\u002Fwww.saas-capital.com\u002Fthe-saas-capital-index\u002F",[1300],"SaaS Capital Index",") — окупаемость хорошо построенного продукта по-прежнему одна из лучших в индустрии.",[26,3756,3758],{"id":3757},"почему-дешёвый-mvp-самый-дорогой","Почему «дешёвый MVP» — самый дорогой?",[11,3760,3761,3762,60],{},"Главный риск сметы — не переплатить, а заплатить дважды. MVP за $1 000 от студии-однодневки почти всегда означает junior-разработчиков без код-ревью, отсутствие тестов и шаблонный код, который невозможно масштабировать. Через 3–6 месяцев такой проект переписывается с нуля — я разобрал эту ловушку с расчётами в статье ",[1270,3763,3765],{"href":3764},"\u002Fru\u002Fblog\u002Fsaas-development-cheap-code-trap","«Цена дешёвого кода»",[11,3767,3768,3769,60],{},"Как выглядит правильная альтернатива по шагам и с архитектурными схемами — в моём руководстве ",[1270,3770,3772],{"href":3771},"\u002Fru\u002Fblog\u002Fb2b-saas-mvp-30-days","«Как собрать MVP для B2B SaaS за 30 дней»",[11,3774,3775,3776,3780,3781,3784],{},"Хотите смету под вашу идею? Опишите продукт на странице ",[1270,3777,3779],{"href":3778},"\u002Fru\u002Fsaas-development","разработки SaaS под ключ"," — я отвечу в течение 24 часов с оценкой бюджета и сроков. Если идея ещё сырая, начните с ",[1270,3782,3783],{"href":1324},"технической консультации",": за час разберём архитектуру, стек и реалистичный план запуска.",{"title":40,"searchDepth":103,"depth":103,"links":3786},[3787,3788,3789,3790],{"id":3574,"depth":103,"text":3575},{"id":3657,"depth":103,"text":3658},{"id":3698,"depth":103,"text":3699},{"id":3757,"depth":103,"text":3758},"2026-06-25","Из чего складывается цена SaaS MVP — от $4 000 за рабочую версию с биллингом. Разбор этапов, сроков, стека и расходов после запуска без маркетинговой воды.",[3794,3797,3800,3803],{"q":3795,"a":3796},"Сколько времени занимает разработка SaaS MVP?","Рабочий MVP с личными кабинетами и биллингом — 4–6 недель. Полноценная платформа с интеграциями, командными аккаунтами и панелью администратора — 2–4 месяца. Сроки считаются от утверждённого технического задания.",{"q":3798,"a":3799},"Что входит в стоимость от $4 000?","Проектирование архитектуры и ТЗ, разработка MVP с регистрацией и личными кабинетами, приём платежей и подписки, панель администратора, деплой на сервер и настройка мониторинга.",{"q":3801,"a":3802},"На каком стеке вы разрабатываете SaaS?","Vue 3 \u002F Nuxt 3 на фронтенде; Node.js, Python (FastAPI) или Laravel на бэкенде; PostgreSQL и Redis для данных; Docker для деплоя. Стек подбирается под задачу, а не наоборот.",{"q":3804,"a":3805},"Что происходит с проектом после запуска?","После запуска я либо беру проект на поддержку (исправление багов, мониторинг, развитие функций), либо передаю его вашей команде с документацией: архитектура, API, инструкции по развёртыванию.","\u002Fimages\u002Fblog\u002Fblog_saas_mvp_cost.jpg",{},"\u002Fblog\u002Fru\u002Fsaas-mvp-development-cost",{"title":3566,"description":3792},"blog\u002Fru\u002Fsaas-mvp-development-cost",[1364,3812,3813,3814],"MVP","Цены","Стартапы","3770TKElJeBZavD7Y_BdNaENdKdfLucuzFoV_Ghf5qY",{"id":3817,"title":3818,"body":3819,"date":4422,"description":4423,"extension":1344,"faq":4424,"image":4434,"lang":1356,"meta":4435,"navigation":106,"path":4436,"published":106,"readTime":213,"seo":4437,"stem":4438,"tags":4439,"updated":1342,"__hash__":4442},"blog\u002Fblog\u002Fru\u002Fb2b-saas-mvp-30-days.md","Как собрать MVP для B2B SaaS за 30 дней без технического долга",{"type":8,"value":3820,"toc":4405},[3821,3824,3831,3835,3838,3841,3847,3851,3854,3861,3865,3868,3892,3896,3906,3926,3930,3933,3937,3940,3944,3955,3963,3966,3970,3973,3977,3980,3984,3995,4001,4005,4008,4365,4367,4392,4402],[11,3822,3823],{},"Запуск Minimum Viable Product (MVP) для B2B SaaS — это сложная балансировка. Запуститесь слишком поздно — упустите рынок, израсходуете инвестиции или построите функции, которые никому не нужны. Запуститесь слишком быстро и небрежно — рискуете закопать проект под горой технического долга, который заблокирует масштабирование, добавление новых фич и рефакторинг уже на второй месяц.",[11,3825,3826,3827,3830],{},"За годы создания коммерческих SaaS-сервисов (таких как TeleGo.io и LingvoHabit) я выработал ",[15,3828,3829],{},"30-дневный фреймворк создания MVP",". Он нацелен на максимальную скорость сборки, идеальную видимость для ИИ-краулеров (GEO) и поисковых систем (SEO), без ущерба для архитектуры. Ниже представлен подробный разбор того, как спроектировать и выкатить рабочий B2B SaaS за 30 дней.",[26,3832,3834],{"id":3833},"главная-проблема-технического-долга-в-стартапах","Главная проблема технического долга в стартапах",[11,3836,3837],{},"Большинство стартапов закрываются не из-за того, что их сервера падают под нагрузкой в миллионы пользователей. Они закрываются потому, что их код превращается в запутанный «спагетти-монстр», где изменение одной строчки ломает авторизацию и платежи.",[11,3839,3840],{},"Чтобы этого избежать, необходимо выбрать стек, который по умолчанию стимулирует разделение ответственности (Separation of Concerns) и обеспечивает быстрое прототипирование.",[34,3842,3845],{"className":3843,"code":3844,"language":39,"meta":40},[37],"+--------------------------------------------------------+\n|                     Nuxt 3 Проект                      |\n| (Универсальный SSR Фронтенд + Мета-данные для SEO\u002FGEO) |\n+---------------------------+----------------------------+\n                            | (HTTP\u002FJSON API)\n                            v\n+--------------------------------------------------------+\n|                 Nitro API Контроллеры                  |\n|    (Безопасная авторизация -> Бизнес-логика служб)     |\n+---------------------------+----------------------------+\n                            | (Запросы к базе данных)\n                            v\n+--------------------------------------------------------+\n|            SQLite База (better-sqlite3)                |\n|    (Транзакции, индексация, сверхбыстрый I\u002FO на диске) |\n+--------------------------------------------------------+\n",[42,3846,3844],{"__ignoreMap":40},[26,3848,3850],{"id":3849},"_1-стек-технологий-для-быстрой-разработки-и-seo","1. Стек технологий для быстрой разработки и SEO",[11,3852,3853],{},"Для запуска за 30 дней нужен стек, обеспечивающий мгновенную реакцию интерфейса, автогенерацию HTML-кода на сервере и простую работу с БД.",[11,3855,3856,3857,3860],{},"Например, при разработке ",[1270,3858,3859],{"href":2862},"MVP платформы LingvoHabit"," (многопользовательский сервис для изучения языков) мы использовали именно такую структуру фронтенда для обеспечения мгновенной загрузки и индексации поисковиками.",[862,3862,3864],{"id":3863},"фронтенд-и-ssr-nuxt-3-vue-3-vite","Фронтенд и SSR: Nuxt 3 (Vue 3, Vite)",[11,3866,3867],{},"Nuxt 3 — золотой стандарт для SaaS-разработки:",[1292,3869,3870,3876,3882],{},[1232,3871,3872,3875],{},[15,3873,3874],{},"Server-Side Rendering (SSR)",": Жизненно необходим для SEO (Google\u002FЯндекс) и GEO (поисковики ChatGPT, Perplexity), которым нужен чистый готовый HTML-код для сканирования и цитирования вашего бренда.",[1232,3877,3878,3881],{},[15,3879,3880],{},"Автоматический роутинг",": Ускоряет верстку новых страниц.",[1232,3883,3884,3887,3888,3891],{},[15,3885,3886],{},"Nitro Engine",": Встроенный серверный движок, позволяющий писать бэкенд-эндпоинты прямо в директории ",[42,3889,3890],{},"\u002Fserver\u002Fapi"," на TypeScript.",[862,3893,3895],{"id":3894},"база-данных-sqlite-через-better-sqlite3","База данных: SQLite через better-sqlite3",[11,3897,3898,3899,3902,3903,60],{},"Не тратьте первую неделю на развертывание кластеров PostgreSQL в облаках AWS или Docker Swarm. Используйте ",[15,3900,3901],{},"SQLite"," с библиотекой ",[42,3904,3905],{},"better-sqlite3",[1292,3907,3908,3914,3920],{},[1232,3909,3910,3913],{},[15,3911,3912],{},"Производительность",": SQLite работает локально в памяти или на SSD, выполняя транзакции за микросекунды. Он без проблем выдерживает 50-100 конкурентных записей в секунду и тысячи чтений — этого хватит для всего периода проверки гипотез.",[1232,3915,3916,3919],{},[15,3917,3918],{},"Мобильность",": Бэкап базы данных — это обычное копирование одного файла.",[1232,3921,3922,3925],{},[15,3923,3924],{},"Путь масштабирования",": Если вы упретесь в лимиты, миграция на PostgreSQL займет не более часа при условии использования ORM или стандартного SQL.",[26,3927,3929],{"id":3928},"_2-чек-лист-4-ключевых-модулей-saas-mvp","2. Чек-лист 4 ключевых модулей SaaS MVP",[11,3931,3932],{},"Для запуска продаж и сбора первых оплат от B2B-клиентов вам нужны всего 4 модуля:",[862,3934,3936],{"id":3935},"а-авторизация-сессии-на-cookie","А. Авторизация (Сессии на Cookie)",[11,3938,3939],{},"Забудьте про сложные интеграции внешних сервисов авторизации на старте. Используйте классическую авторизацию по email\u002Fпаролю или беспарольные Magic Links, сохраняя сессию в безопасных HTTP-only Cookies.",[862,3941,3943],{"id":3942},"б-биллинг-интеграция-stripe-checkout","Б. Биллинг (Интеграция Stripe Checkout)",[11,3945,3946,3947,3950,3951,3954],{},"Используйте готовый сценарий ",[15,3948,3949],{},"Stripe Checkout"," для приема оплат и ",[15,3952,3953],{},"Stripe Customer Portal"," для управления подписками.",[11,3956,3957,3958,3960,3961,60],{},"Например, в проекте ",[1270,3959,1273],{"href":1272}," мы настроили именно эту схему для автоматической обработки сотен активных подписок. Подробный технический разбор этой схемы вы найдете в моей статье ",[1270,3962,1285],{"href":1284},[11,3964,3965],{},"Это избавит вас от необходимости верстать страницы ввода карт, истории счетов и отмены подписки, сэкономив минимум неделю разработки фронтенда.",[862,3967,3969],{"id":3968},"в-core-value-loop-основная-ценность","В. Core Value Loop (Основная ценность)",[11,3971,3972],{},"Это та функция, за которую клиенты готовы платить (например, генерация отчетов, автопостинг, интеграция CRM). Посвятите ей 50% всего времени разработки.",[862,3974,3976],{"id":3975},"г-панель-управления-телеметрия","Г. Панель управления (Телеметрия)",[11,3978,3979],{},"Минималистичный дашборд для основателя, где видны новые регистрации, статусы подписок Stripe и количество выполненных бизнес-операций.",[26,3981,3983],{"id":3982},"_3-масштабируемая-архитектура-никакого-спагетти","3. Масштабируемая архитектура (Никакого спагетти)",[11,3985,3986,3987,3990,3991,3994],{},"Чтобы избежать переписывания кода через 3 месяца, структурируйте проект по ",[15,3988,3989],{},"бизнес-доменам",", а не техническим файлам. Группируйте код логически (а если хотите узнать подробнее о проектировании баз данных под клиентов, изучите мою статью ",[1270,3992,3993],{"href":3688},"Архитектура SaaS баз данных","):",[34,3996,3999],{"className":3997,"code":3998,"language":39,"meta":40},[37],"server\u002F\n├── api\u002F\n│   ├── auth\u002F\n│   ├── billing\u002F\n│   └── projects\u002F\n├── database\u002F\n│   ├── connection.ts\n│   └── schema.sql\n└── services\u002F\n    ├── auth.service.ts\n    ├── stripe.service.ts\n    └── project.service.ts\n",[42,4000,3998],{"__ignoreMap":40},[862,4002,4004],{"id":4003},"пример-реализации-сервис-интеграции-со-stripe","Пример реализации: Сервис интеграции со Stripe",[11,4006,4007],{},"Держите логику оплат отдельно от HTTP-контроллеров. Вот пример типизированного сервиса для генерации ссылки оплаты на TypeScript:",[34,4009,4011],{"className":73,"code":4010,"language":75,"meta":40,"style":40},"\u002F\u002F server\u002Fservices\u002Fstripe.service.ts\nimport Stripe from 'stripe';\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {\n  apiVersion: '2023-10-16',\n});\n\nexport interface SubscriptionInput {\n  email: string;\n  priceId: string;\n  tenantId: string;\n}\n\nexport async function createCheckoutSession(input: SubscriptionInput): Promise\u003Cstring> {\n  const session = await stripe.checkout.sessions.create({\n    payment_method_types: ['card'],\n    line_items: [\n      {\n        price: input.priceId,\n        quantity: 1,\n      },\n    ],\n    mode: 'subscription',\n    success_url: `${process.env.APP_URL}\u002Fdashboard?billing=success`,\n    cancel_url: `${process.env.APP_URL}\u002Fdashboard?billing=cancel`,\n    customer_email: input.email,\n    metadata: {\n      tenantId: input.tenantId,\n    },\n  });\n\n  if (!session.url) {\n    throw new Error('Не удалось сгенерировать URL сессии Stripe.');\n  }\n\n  return session.url;\n}\n",[42,4012,4013,4018,4032,4036,4062,4072,4076,4080,4092,4103,4114,4125,4129,4133,4169,4189,4200,4205,4210,4215,4224,4228,4233,4243,4269,4291,4296,4301,4306,4310,4314,4318,4329,4346,4350,4354,4361],{"__ignoreMap":40},[79,4014,4015],{"class":81,"line":82},[79,4016,4017],{"class":393},"\u002F\u002F server\u002Fservices\u002Fstripe.service.ts\n",[79,4019,4020,4022,4025,4027,4030],{"class":81,"line":103},[79,4021,86],{"class":85},[79,4023,4024],{"class":89}," Stripe ",[79,4026,93],{"class":85},[79,4028,4029],{"class":96}," 'stripe'",[79,4031,100],{"class":89},[79,4033,4034],{"class":81,"line":110},[79,4035,107],{"emptyLinePlaceholder":106},[79,4037,4038,4041,4044,4046,4048,4051,4054,4057,4059],{"class":81,"line":123},[79,4039,4040],{"class":85},"const",[79,4042,4043],{"class":133}," stripe",[79,4045,307],{"class":85},[79,4047,310],{"class":85},[79,4049,4050],{"class":116}," Stripe",[79,4052,4053],{"class":89},"(process.env.",[79,4055,4056],{"class":133},"STRIPE_SECRET_KEY",[79,4058,358],{"class":85},[79,4060,4061],{"class":89},", {\n",[79,4063,4064,4067,4070],{"class":81,"line":139},[79,4065,4066],{"class":89},"  apiVersion: ",[79,4068,4069],{"class":96},"'2023-10-16'",[79,4071,2604],{"class":89},[79,4073,4074],{"class":81,"line":152},[79,4075,3471],{"class":89},[79,4077,4078],{"class":81,"line":165},[79,4079,107],{"emptyLinePlaceholder":106},[79,4081,4082,4084,4087,4090],{"class":81,"line":177},[79,4083,260],{"class":85},[79,4085,4086],{"class":85}," interface",[79,4088,4089],{"class":116}," SubscriptionInput",[79,4091,120],{"class":89},[79,4093,4094,4097,4099,4101],{"class":81,"line":189},[79,4095,4096],{"class":126},"  email",[79,4098,130],{"class":85},[79,4100,147],{"class":133},[79,4102,100],{"class":89},[79,4104,4105,4108,4110,4112],{"class":81,"line":202},[79,4106,4107],{"class":126},"  priceId",[79,4109,130],{"class":85},[79,4111,147],{"class":133},[79,4113,100],{"class":89},[79,4115,4116,4119,4121,4123],{"class":81,"line":208},[79,4117,4118],{"class":126},"  tenantId",[79,4120,130],{"class":85},[79,4122,147],{"class":133},[79,4124,100],{"class":89},[79,4126,4127],{"class":81,"line":213},[79,4128,205],{"class":89},[79,4130,4131],{"class":81,"line":223},[79,4132,107],{"emptyLinePlaceholder":106},[79,4134,4135,4137,4140,4142,4145,4147,4150,4152,4154,4156,4158,4161,4163,4166],{"class":81,"line":235},[79,4136,260],{"class":85},[79,4138,4139],{"class":85}," async",[79,4141,263],{"class":85},[79,4143,4144],{"class":116}," createCheckoutSession",[79,4146,269],{"class":89},[79,4148,4149],{"class":126},"input",[79,4151,130],{"class":85},[79,4153,4089],{"class":116},[79,4155,289],{"class":89},[79,4157,130],{"class":85},[79,4159,4160],{"class":116}," Promise",[79,4162,997],{"class":89},[79,4164,4165],{"class":133},"string",[79,4167,4168],{"class":89},"> {\n",[79,4170,4171,4173,4176,4178,4181,4184,4187],{"class":81,"line":247},[79,4172,301],{"class":85},[79,4174,4175],{"class":133}," session",[79,4177,307],{"class":85},[79,4179,4180],{"class":85}," await",[79,4182,4183],{"class":89}," stripe.checkout.sessions.",[79,4185,4186],{"class":116},"create",[79,4188,888],{"class":89},[79,4190,4191,4194,4197],{"class":81,"line":252},[79,4192,4193],{"class":89},"    payment_method_types: [",[79,4195,4196],{"class":96},"'card'",[79,4198,4199],{"class":89},"],\n",[79,4201,4202],{"class":81,"line":257},[79,4203,4204],{"class":89},"    line_items: [\n",[79,4206,4207],{"class":81,"line":298},[79,4208,4209],{"class":89},"      {\n",[79,4211,4212],{"class":81,"line":319},[79,4213,4214],{"class":89},"        price: input.priceId,\n",[79,4216,4217,4220,4222],{"class":81,"line":343},[79,4218,4219],{"class":89},"        quantity: ",[79,4221,2794],{"class":133},[79,4223,2604],{"class":89},[79,4225,4226],{"class":81,"line":349},[79,4227,3215],{"class":89},[79,4229,4230],{"class":81,"line":364},[79,4231,4232],{"class":89},"    ],\n",[79,4234,4235,4238,4241],{"class":81,"line":379},[79,4236,4237],{"class":89},"    mode: ",[79,4239,4240],{"class":96},"'subscription'",[79,4242,2604],{"class":89},[79,4244,4245,4248,4251,4254,4256,4259,4261,4264,4267],{"class":81,"line":385},[79,4246,4247],{"class":89},"    success_url: ",[79,4249,4250],{"class":96},"`${",[79,4252,4253],{"class":89},"process",[79,4255,60],{"class":96},[79,4257,4258],{"class":89},"env",[79,4260,60],{"class":96},[79,4262,4263],{"class":133},"APP_URL",[79,4265,4266],{"class":96},"}\u002Fdashboard?billing=success`",[79,4268,2604],{"class":89},[79,4270,4271,4274,4276,4278,4280,4282,4284,4286,4289],{"class":81,"line":390},[79,4272,4273],{"class":89},"    cancel_url: ",[79,4275,4250],{"class":96},[79,4277,4253],{"class":89},[79,4279,60],{"class":96},[79,4281,4258],{"class":89},[79,4283,60],{"class":96},[79,4285,4263],{"class":133},[79,4287,4288],{"class":96},"}\u002Fdashboard?billing=cancel`",[79,4290,2604],{"class":89},[79,4292,4293],{"class":81,"line":397},[79,4294,4295],{"class":89},"    customer_email: input.email,\n",[79,4297,4298],{"class":81,"line":450},[79,4299,4300],{"class":89},"    metadata: {\n",[79,4302,4303],{"class":81,"line":455},[79,4304,4305],{"class":89},"      tenantId: input.tenantId,\n",[79,4307,4308],{"class":81,"line":461},[79,4309,3144],{"class":89},[79,4311,4312],{"class":81,"line":474},[79,4313,1195],{"class":89},[79,4315,4316],{"class":81,"line":516},[79,4317,107],{"emptyLinePlaceholder":106},[79,4319,4320,4322,4324,4326],{"class":81,"line":536},[79,4321,352],{"class":85},[79,4323,355],{"class":89},[79,4325,358],{"class":85},[79,4327,4328],{"class":89},"session.url) {\n",[79,4330,4331,4334,4336,4339,4341,4344],{"class":81,"line":541},[79,4332,4333],{"class":85},"    throw",[79,4335,310],{"class":85},[79,4337,4338],{"class":116}," Error",[79,4340,269],{"class":89},[79,4342,4343],{"class":96},"'Не удалось сгенерировать URL сессии Stripe.'",[79,4345,340],{"class":89},[79,4347,4348],{"class":81,"line":547},[79,4349,382],{"class":89},[79,4351,4352],{"class":81,"line":553},[79,4353,107],{"emptyLinePlaceholder":106},[79,4355,4356,4358],{"class":81,"line":566},[79,4357,1204],{"class":85},[79,4359,4360],{"class":89}," session.url;\n",[79,4362,4363],{"class":81,"line":586},[79,4364,205],{"class":89},[26,4366,1290],{"id":1289},[1292,4368,4369,4377,4385],{},[1232,4370,4371,4376],{},[1270,4372,4375],{"href":4373,"rel":4374},"https:\u002F\u002Fnuxt.com\u002Fdocs",[1300],"Документация Nuxt"," — SSR, роутинг и серверные эндпоинты Nitro",[1232,4378,4379,4384],{},[1270,4380,4383],{"href":4381,"rel":4382},"https:\u002F\u002Fwww.sqlite.org\u002Fwhentouse.html",[1300],"SQLite: Appropriate Uses"," — официальные рекомендации, когда SQLite уместен в продакшене",[1232,4386,4387,4391],{},[1270,4388,3949],{"href":4389,"rel":4390},"https:\u002F\u002Fdocs.stripe.com\u002Fpayments\u002Fcheckout",[1300]," — готовый платёжный сценарий, использованный в примере выше",[11,4393,4394,4395,1321,4398,4401],{},"Если вы планируете разработку B2B SaaS MVP и ищете опытного технического лида для запуска продукта за несколько недель, ознакомьтесь с моей услугой ",[1270,4396,4397],{"href":3778},"Разработка SaaS под ключ",[1270,4399,4400],{"href":1324},"Техническую консультацию",", чтобы детально проработать архитектуру вашего проекта.",[1328,4403,4404],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 .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":4406},[4407,4408,4412,4418,4421],{"id":3833,"depth":103,"text":3834},{"id":3849,"depth":103,"text":3850,"children":4409},[4410,4411],{"id":3863,"depth":110,"text":3864},{"id":3894,"depth":110,"text":3895},{"id":3928,"depth":103,"text":3929,"children":4413},[4414,4415,4416,4417],{"id":3935,"depth":110,"text":3936},{"id":3942,"depth":110,"text":3943},{"id":3968,"depth":110,"text":3969},{"id":3975,"depth":110,"text":3976},{"id":3982,"depth":103,"text":3983,"children":4419},[4420],{"id":4003,"depth":110,"text":4004},{"id":1289,"depth":103,"text":1290},"2026-06-21","Практическое руководство для основателей стартапов по быстрому запуску масштабируемого SaaS-продукта на базе современного легковесного стека, баз данных SQLite и модульной архитектуры.",[4425,4428,4431],{"q":4426,"a":4427},"Действительно ли можно запускать продакшен на SQLite?","Да. SQLite — надёжная база данных: в режиме WAL (Write-Ahead Logging) она читает быстрее многих клиент-серверных СУБД. Для бутстрэп-стартапа это идеальный вариант, минимизирующий расходы на хостинг на этапе проверки гипотез.",{"q":4429,"a":4430},"Когда переходить с SQLite на PostgreSQL?","Когда потребуется горизонтальное масштабирование (несколько серверов приложений — SQLite работает с локальным диском) или когда размер базы превысит ~100 ГБ. При использовании ORM или стандартного SQL миграция занимает считаные часы.",{"q":4432,"a":4433},"Сколько стоит SaaS MVP такого объёма?","В моей практике MVP с авторизацией, биллингом Stripe и панелью управления стоит от $4 000 и занимает 4–6 недель от утверждённого технического задания. Половина бюджета уходит на ядро продукта — функцию, за которую платят клиенты.","\u002Fimages\u002Fblog\u002Fblog_saas_mvp.jpg",{},"\u002Fblog\u002Fru\u002Fb2b-saas-mvp-30-days",{"title":3818,"description":4423},"blog\u002Fru\u002Fb2b-saas-mvp-30-days",[1364,3812,4440,4441,1365],"Архитектура","Nuxt","ntlbujBjIEWiN5rOb6rQn9WW5_mXyrWDaIRsXTyF28M",{"id":4444,"title":4445,"body":4446,"date":4729,"description":4730,"extension":1344,"faq":4731,"image":4741,"lang":1356,"meta":4742,"navigation":106,"path":4743,"published":106,"readTime":213,"seo":4744,"stem":4745,"tags":4746,"updated":1342,"__hash__":4750},"blog\u002Fblog\u002Fru\u002Fsaas-development-cheap-code-trap.md","Цена дешевого кода: Почему приходится переписывать SaaS MVP с нуля",{"type":8,"value":4447,"toc":4720},[4448,4451,4454,4457,4460,4464,4467,4487,4494,4498,4501,4577,4581,4587,4593,4596,4634,4638,4656,4660,4663,4692,4694,4711],[11,4449,4450],{},"Снижение первоначальных затрат на разработку — естественное желание любого фаундера стартапа на ранней стадии. Это стремление часто приводит к найму дешевых фрилансеров или бюджетных аутсорсинговых агентств для сборки первой версии (MVP) своего SaaS-продукта.",[11,4452,4453],{},"Обещание выглядит крайне заманчиво: полноценное B2B-приложение, готовое к продажам за долю от стоимости услуг senior-разработчика.",[11,4455,4456],{},"Однако большинство таких проектов сталкиваются с жесткой реальностью уже через пару месяцев после запуска. Добавление простого элемента интерфейса ломает систему биллинга, время загрузки страниц превышает 8 секунд, а новые разработчики наотрез отказываются поддерживать код и рекомендуют «переписать все с нуля».",[11,4458,4459],{},"В этом руководстве для фаундеров мы разберем причины возникновения этой ловушки, опишем чеклист оценки здоровья кода и дадим математическую формулу выбора между рефакторингом и переписыванием.",[26,4461,4463],{"id":4462},"экономика-дешевой-разработки","Экономика дешевой разработки",[11,4465,4466],{},"Когда веб-студия берет за разработку сложного SaaS-продукта небольшие деньги, она вынуждена жестко экономить на процессах, чтобы сохранить свою маржинальность. Экономия происходит за счет трех факторов:",[1229,4468,4469,4475,4481],{},[1232,4470,4471,4474],{},[15,4472,4473],{},"Привлечение начинающих специалистов",": Проектированием архитектуры и написанием логики занимаются программисты уровня Junior, не имеющие опыта работы с нагруженными системами.",[1232,4476,4477,4480],{},[15,4478,4479],{},"Отсутствие контроля качества",": В коде полностью отсутствуют автотесты и документация к API. Тестирование проводится вручную самим разработчиком.",[1232,4482,4483,4486],{},[15,4484,4485],{},"Использование жестких шаблонов",": Проект собирается на базе готовых бесплатных конструкторов или старых модулей, которые невозможно масштабировать и интегрировать со сложными внешними сервисами.",[11,4488,4489,4490,4493],{},"В результате с первого дня разработки накапливается огромный ",[15,4491,4492],{},"технический долг",". Экономия на старте превращается в гигантские расходы на исправление багов и крайне низкую скорость выкатывания новых функций в дальнейшем.",[26,4495,4497],{"id":4496},"_1-чеклист-технического-анализа-mvp","1. Чеклист технического анализа MVP",[11,4499,4500],{},"Чтобы принять объективное решение, оцените вашу кодовую базу по ключевым техническим критериям:",[2415,4502,4503,4516],{},[2418,4504,4505],{},[2421,4506,4507,4510,4513],{},[2424,4508,4509],{"align":2426},"Слой разработки",[2424,4511,4512],{"align":2426},"Рефакторинг целесообразен",[2424,4514,4515],{"align":2426},"Требуется переписать с нуля",[2435,4517,4518,4531,4544,4560],{},[2421,4519,4520,4525,4528],{},[2440,4521,4522],{"align":2426},[15,4523,4524],{},"Структура БД",[2440,4526,4527],{"align":2426},"Схемы данных нормализованы, настроены связи (Foreign Keys) и индексы.",[2440,4529,4530],{"align":2426},"Связи отсутствуют, таблицы избыточны, данные постоянно дублируются и искажаются.",[2421,4532,4533,4538,4541],{},[2440,4534,4535],{"align":2426},[15,4536,4537],{},"Изоляция логики",[2440,4539,4540],{"align":2426},"Бизнес-логика вынесена в сервисы, визуальные компоненты занимаются только отрисовкой UI.",[2440,4542,4543],{"align":2426},"SQL-запросы к базе данных или секретные ключи (например, Stripe) прописаны прямо внутри клиентских компонентов.",[2421,4545,4546,4551,4554],{},[2440,4547,4548],{"align":2426},[15,4549,4550],{},"Управление зависимостями",[2440,4552,4553],{"align":2426},"Используются стандартные npm-библиотеки, которые можно обновить.",[2440,4555,4556,4557,60],{"align":2426},"Используются самописные или заброшенные пакеты, модифицированные вручную в ",[42,4558,4559],{},"node_modules",[2421,4561,4562,4567,4570],{},[2440,4563,4564],{"align":2426},[15,4565,4566],{},"Обработка ошибок",[2440,4568,4569],{"align":2426},"Ошибки перехватываются, пишутся в лог и отдают корректные HTTP-коды (400, 401, 500).",[2440,4571,4572,4573,4576],{"align":2426},"Пустые блоки ",[42,4574,4575],{},"try {} catch(e) {}",", которые маскируют ошибки. Приложение падает без записи в логи.",[26,4578,4580],{"id":4579},"_2-математика-решения-рефакторинг-против-переписывания","2. Математика решения: Рефакторинг против Переписывания",[11,4582,4583,4584,130],{},"Чтобы определить наиболее выгодный путь для бизнеса, рассчитайте ",[15,4585,4586],{},"Коэффициент Рефакторинга (K)",[34,4588,4591],{"className":4589,"code":4590,"language":39},[37],"K = (C_audit + C_tests + C_db + C_logic) \u002F C_rebuild\n",[42,4592,4590],{"__ignoreMap":40},[11,4594,4595],{},"Где:",[1292,4597,4598,4604,4610,4616,4622,4628],{},[1232,4599,4600,4603],{},[15,4601,4602],{},"K",": Коэффициент Рефакторинга (порог целесообразности).",[1232,4605,4606,4609],{},[15,4607,4608],{},"C_audit",": Стоимость аудита и поиска скрытых багов в текущем легаси-коде.",[1232,4611,4612,4615],{},[15,4613,4614],{},"C_tests",": Стоимость написания автотестов, чтобы гарантировать, что рефакторинг не сломает работающие функции.",[1232,4617,4618,4621],{},[15,4619,4620],{},"C_db",": Стоимость исправления структуры БД и миграции данных текущих пользователей без потерь.",[1232,4623,4624,4627],{},[15,4625,4626],{},"C_logic",": Стоимость выноса бизнес-логики в чистые сервисы.",[1232,4629,4630,4633],{},[15,4631,4632],{},"C_rebuild",": Стоимость разработки этого же функционала с нуля на чистой архитектуре (например, Nuxt 3 + Node.js).",[862,4635,4637],{"id":4636},"правило-принятия-решения","Правило принятия решения:",[1292,4639,4640,4647],{},[1232,4641,4642,4643,4646],{},"Если ",[15,4644,4645],{},"K \u003C 0.5",": Рефакторинг выгоден. Фундамент кода прочный, достаточно провести генеральную уборку.",[1232,4648,4642,4649,2598,4652,4655],{},[15,4650,4651],{},"K >= 0.7",[15,4653,4654],{},"Переписывайте проект",". Попытка исправить плохой код обойдется дороже, чем создание чистого аналога. Это как ремонт аварийного фундамента — латание трещин заберет больше ресурсов, чем заливка новой качественной плиты.",[26,4657,4659],{"id":4658},"_3-как-защитить-проект-при-перезапуске-разработки","3. Как защитить проект при перезапуске разработки",[11,4661,4662],{},"Если вы приняли решение о переписывании кода, внедрите стандарты контроля подрядчиков:",[1292,4664,4665,4675,4681],{},[1232,4666,4667,4670,4671,4674],{},[15,4668,4669],{},"Контроль репозитория",": Исходный код должен находиться строго в вашем аккаунте GitHub\u002FGitLab с запретом прямых коммитов в ветку ",[42,4672,4673],{},"main"," без код-ревью.",[1232,4676,4677,4680],{},[15,4678,4679],{},"Автоматизация проверок",": Обязайте команду использовать линтеры кода (ESLint, Biome) и покрывать автотестами ключевую бизнес-логику (не менее 70% покрытия).",[1232,4682,4683,4686,4687,4691],{},[15,4684,4685],{},"Внешний контроль на ранних этапах",": Не ждите конца разработки. Наймите независимого архитектора для аудита кода на 2-3 неделе разработки, чтобы пресечь системные ошибки в самом начале; вы можете ознакомиться с моим полным ",[1270,4688,4690],{"href":4689},"\u002Fru\u002Fblog\u002Fcode-audit-technical-debt","Чек-листом аудита кодовой базы",", чтобы понять, как устроен этот процесс.",[26,4693,1290],{"id":1289},[1292,4695,4696,4704],{},[1232,4697,4698,4703],{},[1270,4699,4702],{"href":4700,"rel":4701},"https:\u002F\u002Fmartinfowler.com\u002Fbliki\u002FTechnicalDebt.html",[1300],"Martin Fowler: Technical Debt"," — каноническое определение техдолга и его квадрантов",[1232,4705,4706,4710],{},[1270,4707,4709],{"href":2561,"rel":4708},[1300],"OWASP Top 10"," — типовые уязвимости, которые чаще всего находят в «дешёвом» коде",[11,4712,4713,4714,1321,4717,4719],{},"Если ваш сервис работает медленно, ломается при каждом обновлении или вам нужна объективная техническая оценка кодовой базы стартапа, изучите мою услугу ",[1270,4715,4716],{"href":3778},"Разработки SaaS-платформ",[1270,4718,1325],{"href":1324}," для проведения аудита.",{"title":40,"searchDepth":103,"depth":103,"links":4721},[4722,4723,4724,4727,4728],{"id":4462,"depth":103,"text":4463},{"id":4496,"depth":103,"text":4497},{"id":4579,"depth":103,"text":4580,"children":4725},[4726],{"id":4636,"depth":110,"text":4637},{"id":4658,"depth":103,"text":4659},{"id":1289,"depth":103,"text":1290},"2026-06-18","Почему выбор дешевых подрядчиков часто приводит к необходимости полной переработки проекта, и как математически рассчитать целесообразность рефакторинга.",[4732,4735,4738],{"q":4733,"a":4734},"Как быстро понять, что MVP придётся переписывать?","Четыре красных флага: в базе данных нет связей и индексов, бизнес-логика зашита в клиентские компоненты вместе с секретными ключами, зависимости заброшены или правлены вручную в node_modules, ошибки глушатся пустыми try\u002Fcatch без логов. Два и более флага — почти всегда переписывание.",{"q":4736,"a":4737},"Когда рефакторинг выгоднее переписывания?","Когда суммарная стоимость аудита, тестов, исправления базы и выноса логики меньше половины стоимости разработки с нуля (коэффициент K_R \u003C 0.5). Если K_R ≥ 0.7 — дешевле переписать: латание аварийного фундамента заберёт больше ресурсов, чем новая плита.",{"q":4739,"a":4740},"Как не попасть в ту же ловушку со вторым подрядчиком?","Держите код в своём репозитории с запретом коммитов в main без ревью, требуйте линтеры и покрытие ключевой логики тестами от 70%, и закажите независимый аудит архитектуры на 2–3 неделе разработки, а не после релиза.","\u002Fimages\u002Fblog\u002Fblog_cheap_code_trap.jpg",{},"\u002Fblog\u002Fru\u002Fsaas-development-cheap-code-trap",{"title":4445,"description":4730},"blog\u002Fru\u002Fsaas-development-cheap-code-trap",[1364,3814,4747,4748,4749],"Консультации","Управление","Аутсорсинг","C8Hgh7N_bl4zzkiyeBNB8dtPqqzFYKzwgG1WcT9xj_E",{"id":4752,"title":4753,"body":4754,"date":6072,"description":6073,"extension":1344,"faq":6074,"image":6084,"lang":1356,"meta":6085,"navigation":106,"path":6086,"published":106,"readTime":213,"seo":6087,"stem":6088,"tags":6089,"updated":1342,"__hash__":6093},"blog\u002Fblog\u002Fru\u002Fsaas-stripe-billing-integration.md","Монетизация SaaS: Как безболезненно настроить Stripe, подписки и биллинг",{"type":8,"value":4755,"toc":6066},[4756,4759,4762,4765,4769,4772,4778,4808,4812,4826,4829,5984,5988,6012,6021,6023,6049,6055,6063],[11,4757,4758],{},"Интеграция рекуррентных платежей в SaaS-продукты обманчиво проста на словах. Но если разовое списание средств с карты реализуется быстро, то управление жизненным циклом подписок вскрывает десятки подводных камней: отслеживание истечения срока действия карт, интервалы повторных попыток списания, апгрейды и даунгрейды тарифных планов, льготные периоды (grace periods) и обработка вебхуков.",[11,4760,4761],{},"Если вебхук от платежной системы обработан некорректно или не в том порядке, вы рискуете заблокировать платящих клиентов либо предоставить бесплатный доступ тем, кто уже отменил подписку.",[11,4763,4764],{},"В этом практическом руководстве мы настроим отказоустойчивый обработчик Stripe Webhooks с проверкой подписи на Node.js\u002FTypeScript и опишем процесс синхронизации данных.",[26,4766,4768],{"id":4767},"_1-анатомия-жизненного-цикла-подписки","1. Анатомия жизненного цикла подписки",[11,4770,4771],{},"Ваша база данных должна точно отслеживать статус подписки клиента. Stripe использует несколько ключевых статусов, которые вы должны синхронизировать со своей БД:",[34,4773,4776],{"className":4774,"code":4775,"language":39,"meta":40},[37],"               +--------------------------------------+\n               |               trialing               |\n               +------------------+-------------------+\n                                  | (Триал завершен или оформлена оплата)\n                                  v\n+------------------->  +----------------------+\n|                      |        active        | \u003C------------------+\n|                      +----------+-----------+                    |\n| (Карта обновлена,               |                                |\n|  списание успешно)              | (Списание не прошло, Stripe        | (Продление\n|                                 |  повторяет попытки)                |  до окончания\n|                                 v                                |  периода)\n|                      +----------+-----------+                    |\n+---------------------+|       past_due       |                    |\n                       +----------+-----------+                    |\n                                  | (Все попытки списания провалились) |\n                                  v                                |\n                       +----------+-----------+                    |\n                       |       canceled       | -------------------+\n                       +----------------------+\n",[42,4777,4775],{"__ignoreMap":40},[1292,4779,4780,4786,4792,4802],{},[1232,4781,4782,4785],{},[42,4783,4784],{},"trialing",": Бесплатный ознакомительный период. Доступ открыт.",[1232,4787,4788,4791],{},[42,4789,4790],{},"active",": Платеж прошел успешно. Полный доступ.",[1232,4793,4794,4797,4798,4801],{},[42,4795,4796],{},"past_due",": Очередной платеж не прошел. ",[2410,4799,4800],{},"Не блокируйте пользователя сразу",". Переведите аккаунт в предупредительный статус (льготный период от 3 до 7 дней), отключите ресурсоемкие API-методы и отправьте напоминание об обновлении платежных данных.",[1232,4803,4804,4807],{},[42,4805,4806],{},"canceled",": Подписка полностью аннулирована. Доступ закрыт. Для возобновления клиент должен заново привязать карту.",[26,4809,4811],{"id":4810},"_2-реализация-обработчика-stripe-webhooks-с-верификацией-подписи","2. Реализация обработчика Stripe Webhooks с верификацией подписи",[11,4813,4814,4815,4818,4819,4822,4823,60],{},"Stripe отправляет асинхронные уведомления об изменениях статусов через HTTP POST запросы. Поскольку любой злоумышленник может отправить поддельный запрос на ваш эндпоинт ",[42,4816,4817],{},"\u002Fapi\u002Fbilling\u002Fwebhook",", вы ",[15,4820,4821],{},"обязаны валидировать криптографическую подпись"," Stripe в заголовке ",[42,4824,4825],{},"stripe-signature",[11,4827,4828],{},"Пример реализации обработчика на Node.js\u002FTypeScript и Express:",[34,4830,4832],{"className":73,"code":4831,"language":75,"meta":40,"style":40},"import express from 'express';\nimport Stripe from 'stripe';\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {\n  apiVersion: '2023-10-16',\n});\n\nconst app = express();\nconst endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;\n\n\u002F\u002F ВАЖНО: Проверка подписи Stripe требует сырого буфера тела запроса (raw body).\n\u002F\u002F Не применяйте bodyParser.json() к этому маршруту до проверки подписи.\napp.post(\n  '\u002Fapi\u002Fbilling\u002Fwebhook',\n  express.raw({ type: 'application\u002Fjson' }),\n  async (req: express.Request, res: express.Response) => {\n    const sig = req.headers['stripe-signature'];\n    \n    if (!sig) {\n      return res.status(400).send('Отсутствует заголовок stripe-signature.');\n    }\n\n    let event: Stripe.Event;\n\n    try {\n      \u002F\u002F Проверяем подпись вебхука, используя секрет из кабинета Stripe\n      event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);\n    } catch (err: any) {\n      console.error(`[Security Warning] Ошибка проверки подписи: ${err.message}`);\n      return res.status(400).send(`Webhook Error: ${err.message}`);\n    }\n\n    try {\n      switch (event.type) {\n        case 'customer.subscription.created':\n        case 'customer.subscription.updated': {\n          const subscription = event.data.object as Stripe.Subscription;\n          await handleSubscriptionUpdate(subscription);\n          break;\n        }\n        case 'customer.subscription.deleted': {\n          const subscription = event.data.object as Stripe.Subscription;\n          await handleSubscriptionDeletion(subscription);\n          break;\n        }\n        case 'invoice.payment_failed': {\n          const invoice = event.data.object as Stripe.Invoice;\n          await handlePaymentFailure(invoice);\n          break;\n        }\n        default:\n          console.log(`[Stripe Webhook] Пропущено событие: ${event.type}`);\n      }\n\n      res.json({ received: true });\n    } catch (err: any) {\n      console.error(`[Webhook Process Error] Ошибка обработки события: ${err.message}`);\n      res.status(500).send('Internal Server Error');\n    }\n  }\n);\n\nasync function handleSubscriptionUpdate(sub: Stripe.Subscription) {\n  const tenantId = sub.metadata.tenantId;\n  if (!tenantId) {\n    throw new Error(`В метаданных подписки ${sub.id} отсутствует ID клиента (tenantId).`);\n  }\n\n  const status = sub.status; \u002F\u002F 'active', 'trialing', 'past_due' и др.\n  const priceId = sub.items.data[0].price.id;\n  const currentPeriodEnd = new Date(sub.current_period_end * 1000);\n\n  \u002F\u002F Синхронизируем статус в нашей базе данных\n  await db.query(\n    `UPDATE tenants \n     SET subscription_status = $1, stripe_price_id = $2, subscription_period_end = $3\n     WHERE id = $4`,\n    [status, priceId, currentPeriodEnd, tenantId]\n  );\n\n  console.log(`[Billing Sync] Статус клиента ${tenantId} обновлен на: ${status}`);\n}\n\nasync function handleSubscriptionDeletion(sub: Stripe.Subscription) {\n  const tenantId = sub.metadata.tenantId;\n  if (!tenantId) return;\n\n  await db.query(\n    `UPDATE tenants \n     SET subscription_status = 'canceled', stripe_price_id = NULL\n     WHERE id = $1`,\n    [tenantId]\n  );\n\n  console.log(`[Billing Sync] Подписка клиента ${tenantId} аннулирована.`);\n}\n\nasync function handlePaymentFailure(invoice: Stripe.Invoice) {\n  const customerId = invoice.customer as string;\n  \u002F\u002F Находим пользователя по customerId и отправляем предупреждающее письмо\n  console.warn(`[Billing Alert] Ошибка оплаты счета для клиента ${customerId}`);\n}\n",[42,4833,4834,4848,4860,4864,4884,4892,4896,4900,4914,4933,4937,4942,4947,4958,4965,4982,5022,5040,5045,5056,5084,5088,5092,5111,5115,5122,5127,5143,5162,5186,5217,5221,5225,5231,5239,5250,5259,5283,5294,5301,5305,5314,5334,5343,5349,5353,5362,5384,5394,5400,5404,5411,5436,5440,5444,5459,5475,5496,5518,5522,5526,5530,5535,5560,5573,5585,5611,5616,5621,5637,5656,5682,5687,5693,5707,5713,5719,5727,5733,5739,5744,5769,5774,5779,5802,5813,5830,5835,5846,5851,5857,5865,5871,5876,5881,5900,5905,5910,5934,5953,5959,5979],{"__ignoreMap":40},[79,4835,4836,4838,4841,4843,4846],{"class":81,"line":82},[79,4837,86],{"class":85},[79,4839,4840],{"class":89}," express ",[79,4842,93],{"class":85},[79,4844,4845],{"class":96}," 'express'",[79,4847,100],{"class":89},[79,4849,4850,4852,4854,4856,4858],{"class":81,"line":103},[79,4851,86],{"class":85},[79,4853,4024],{"class":89},[79,4855,93],{"class":85},[79,4857,4029],{"class":96},[79,4859,100],{"class":89},[79,4861,4862],{"class":81,"line":110},[79,4863,107],{"emptyLinePlaceholder":106},[79,4865,4866,4868,4870,4872,4874,4876,4878,4880,4882],{"class":81,"line":123},[79,4867,4040],{"class":85},[79,4869,4043],{"class":133},[79,4871,307],{"class":85},[79,4873,310],{"class":85},[79,4875,4050],{"class":116},[79,4877,4053],{"class":89},[79,4879,4056],{"class":133},[79,4881,358],{"class":85},[79,4883,4061],{"class":89},[79,4885,4886,4888,4890],{"class":81,"line":139},[79,4887,4066],{"class":89},[79,4889,4069],{"class":96},[79,4891,2604],{"class":89},[79,4893,4894],{"class":81,"line":152},[79,4895,3471],{"class":89},[79,4897,4898],{"class":81,"line":165},[79,4899,107],{"emptyLinePlaceholder":106},[79,4901,4902,4904,4907,4909,4912],{"class":81,"line":177},[79,4903,4040],{"class":85},[79,4905,4906],{"class":133}," app",[79,4908,307],{"class":85},[79,4910,4911],{"class":116}," express",[79,4913,447],{"class":89},[79,4915,4916,4918,4921,4923,4926,4929,4931],{"class":81,"line":189},[79,4917,4040],{"class":85},[79,4919,4920],{"class":133}," endpointSecret",[79,4922,307],{"class":85},[79,4924,4925],{"class":89}," process.env.",[79,4927,4928],{"class":133},"STRIPE_WEBHOOK_SECRET",[79,4930,358],{"class":85},[79,4932,100],{"class":89},[79,4934,4935],{"class":81,"line":202},[79,4936,107],{"emptyLinePlaceholder":106},[79,4938,4939],{"class":81,"line":208},[79,4940,4941],{"class":393},"\u002F\u002F ВАЖНО: Проверка подписи Stripe требует сырого буфера тела запроса (raw body).\n",[79,4943,4944],{"class":81,"line":213},[79,4945,4946],{"class":393},"\u002F\u002F Не применяйте bodyParser.json() к этому маршруту до проверки подписи.\n",[79,4948,4949,4952,4955],{"class":81,"line":223},[79,4950,4951],{"class":89},"app.",[79,4953,4954],{"class":116},"post",[79,4956,4957],{"class":89},"(\n",[79,4959,4960,4963],{"class":81,"line":235},[79,4961,4962],{"class":96},"  '\u002Fapi\u002Fbilling\u002Fwebhook'",[79,4964,2604],{"class":89},[79,4966,4967,4970,4973,4976,4979],{"class":81,"line":247},[79,4968,4969],{"class":89},"  express.",[79,4971,4972],{"class":116},"raw",[79,4974,4975],{"class":89},"({ type: ",[79,4977,4978],{"class":96},"'application\u002Fjson'",[79,4980,4981],{"class":89}," }),\n",[79,4983,4984,4987,4989,4992,4994,4996,4998,5001,5003,5006,5008,5010,5012,5015,5018,5020],{"class":81,"line":252},[79,4985,4986],{"class":85},"  async",[79,4988,355],{"class":89},[79,4990,4991],{"class":126},"req",[79,4993,130],{"class":85},[79,4995,4911],{"class":116},[79,4997,60],{"class":89},[79,4999,5000],{"class":116},"Request",[79,5002,279],{"class":89},[79,5004,5005],{"class":126},"res",[79,5007,130],{"class":85},[79,5009,4911],{"class":116},[79,5011,60],{"class":89},[79,5013,5014],{"class":116},"Response",[79,5016,5017],{"class":89},") ",[79,5019,1023],{"class":85},[79,5021,120],{"class":89},[79,5023,5024,5026,5029,5031,5034,5037],{"class":81,"line":257},[79,5025,753],{"class":85},[79,5027,5028],{"class":133}," sig",[79,5030,307],{"class":85},[79,5032,5033],{"class":89}," req.headers[",[79,5035,5036],{"class":96},"'stripe-signature'",[79,5038,5039],{"class":89},"];\n",[79,5041,5042],{"class":81,"line":298},[79,5043,5044],{"class":89},"    \n",[79,5046,5047,5049,5051,5053],{"class":81,"line":319},[79,5048,1051],{"class":85},[79,5050,355],{"class":89},[79,5052,358],{"class":85},[79,5054,5055],{"class":89},"sig) {\n",[79,5057,5058,5061,5064,5067,5069,5072,5074,5077,5079,5082],{"class":81,"line":343},[79,5059,5060],{"class":85},"      return",[79,5062,5063],{"class":89}," res.",[79,5065,5066],{"class":116},"status",[79,5068,269],{"class":89},[79,5070,5071],{"class":133},"400",[79,5073,441],{"class":89},[79,5075,5076],{"class":116},"send",[79,5078,269],{"class":89},[79,5080,5081],{"class":96},"'Отсутствует заголовок stripe-signature.'",[79,5083,340],{"class":89},[79,5085,5086],{"class":81,"line":349},[79,5087,1190],{"class":89},[79,5089,5090],{"class":81,"line":364},[79,5091,107],{"emptyLinePlaceholder":106},[79,5093,5094,5097,5100,5102,5104,5106,5109],{"class":81,"line":379},[79,5095,5096],{"class":85},"    let",[79,5098,5099],{"class":89}," event",[79,5101,130],{"class":85},[79,5103,4050],{"class":116},[79,5105,60],{"class":89},[79,5107,5108],{"class":116},"Event",[79,5110,100],{"class":89},[79,5112,5113],{"class":81,"line":385},[79,5114,107],{"emptyLinePlaceholder":106},[79,5116,5117,5120],{"class":81,"line":390},[79,5118,5119],{"class":85},"    try",[79,5121,120],{"class":89},[79,5123,5124],{"class":81,"line":397},[79,5125,5126],{"class":393},"      \u002F\u002F Проверяем подпись вебхука, используя секрет из кабинета Stripe\n",[79,5128,5129,5132,5134,5137,5140],{"class":81,"line":450},[79,5130,5131],{"class":89},"      event ",[79,5133,1090],{"class":85},[79,5135,5136],{"class":89}," stripe.webhooks.",[79,5138,5139],{"class":116},"constructEvent",[79,5141,5142],{"class":89},"(req.body, sig, endpointSecret);\n",[79,5144,5145,5148,5150,5152,5155,5157,5159],{"class":81,"line":455},[79,5146,5147],{"class":89},"    } ",[79,5149,829],{"class":85},[79,5151,355],{"class":89},[79,5153,5154],{"class":126},"err",[79,5156,130],{"class":85},[79,5158,1043],{"class":133},[79,5160,5161],{"class":89},") {\n",[79,5163,5164,5167,5170,5172,5175,5177,5179,5182,5184],{"class":81,"line":461},[79,5165,5166],{"class":89},"      console.",[79,5168,5169],{"class":116},"error",[79,5171,269],{"class":89},[79,5173,5174],{"class":96},"`[Security Warning] Ошибка проверки подписи: ${",[79,5176,5154],{"class":89},[79,5178,60],{"class":96},[79,5180,5181],{"class":89},"message",[79,5183,510],{"class":96},[79,5185,340],{"class":89},[79,5187,5188,5190,5192,5194,5196,5198,5200,5202,5204,5207,5209,5211,5213,5215],{"class":81,"line":474},[79,5189,5060],{"class":85},[79,5191,5063],{"class":89},[79,5193,5066],{"class":116},[79,5195,269],{"class":89},[79,5197,5071],{"class":133},[79,5199,441],{"class":89},[79,5201,5076],{"class":116},[79,5203,269],{"class":89},[79,5205,5206],{"class":96},"`Webhook Error: ${",[79,5208,5154],{"class":89},[79,5210,60],{"class":96},[79,5212,5181],{"class":89},[79,5214,510],{"class":96},[79,5216,340],{"class":89},[79,5218,5219],{"class":81,"line":516},[79,5220,1190],{"class":89},[79,5222,5223],{"class":81,"line":536},[79,5224,107],{"emptyLinePlaceholder":106},[79,5226,5227,5229],{"class":81,"line":541},[79,5228,5119],{"class":85},[79,5230,120],{"class":89},[79,5232,5233,5236],{"class":81,"line":547},[79,5234,5235],{"class":85},"      switch",[79,5237,5238],{"class":89}," (event.type) {\n",[79,5240,5241,5244,5247],{"class":81,"line":553},[79,5242,5243],{"class":85},"        case",[79,5245,5246],{"class":96}," 'customer.subscription.created'",[79,5248,5249],{"class":89},":\n",[79,5251,5252,5254,5257],{"class":81,"line":566},[79,5253,5243],{"class":85},[79,5255,5256],{"class":96}," 'customer.subscription.updated'",[79,5258,2660],{"class":89},[79,5260,5261,5264,5267,5269,5272,5274,5276,5278,5281],{"class":81,"line":586},[79,5262,5263],{"class":85},"          const",[79,5265,5266],{"class":133}," subscription",[79,5268,307],{"class":85},[79,5270,5271],{"class":89}," event.data.object ",[79,5273,1040],{"class":85},[79,5275,4050],{"class":116},[79,5277,60],{"class":89},[79,5279,5280],{"class":116},"Subscription",[79,5282,100],{"class":89},[79,5284,5285,5288,5291],{"class":81,"line":597},[79,5286,5287],{"class":85},"          await",[79,5289,5290],{"class":116}," handleSubscriptionUpdate",[79,5292,5293],{"class":89},"(subscription);\n",[79,5295,5296,5299],{"class":81,"line":607},[79,5297,5298],{"class":85},"          break",[79,5300,100],{"class":89},[79,5302,5303],{"class":81,"line":612},[79,5304,3323],{"class":89},[79,5306,5307,5309,5312],{"class":81,"line":618},[79,5308,5243],{"class":85},[79,5310,5311],{"class":96}," 'customer.subscription.deleted'",[79,5313,2660],{"class":89},[79,5315,5316,5318,5320,5322,5324,5326,5328,5330,5332],{"class":81,"line":630},[79,5317,5263],{"class":85},[79,5319,5266],{"class":133},[79,5321,307],{"class":85},[79,5323,5271],{"class":89},[79,5325,1040],{"class":85},[79,5327,4050],{"class":116},[79,5329,60],{"class":89},[79,5331,5280],{"class":116},[79,5333,100],{"class":89},[79,5335,5336,5338,5341],{"class":81,"line":644},[79,5337,5287],{"class":85},[79,5339,5340],{"class":116}," handleSubscriptionDeletion",[79,5342,5293],{"class":89},[79,5344,5345,5347],{"class":81,"line":654},[79,5346,5298],{"class":85},[79,5348,100],{"class":89},[79,5350,5351],{"class":81,"line":668},[79,5352,3323],{"class":89},[79,5354,5355,5357,5360],{"class":81,"line":673},[79,5356,5243],{"class":85},[79,5358,5359],{"class":96}," 'invoice.payment_failed'",[79,5361,2660],{"class":89},[79,5363,5364,5366,5369,5371,5373,5375,5377,5379,5382],{"class":81,"line":679},[79,5365,5263],{"class":85},[79,5367,5368],{"class":133}," invoice",[79,5370,307],{"class":85},[79,5372,5271],{"class":89},[79,5374,1040],{"class":85},[79,5376,4050],{"class":116},[79,5378,60],{"class":89},[79,5380,5381],{"class":116},"Invoice",[79,5383,100],{"class":89},[79,5385,5386,5388,5391],{"class":81,"line":698},[79,5387,5287],{"class":85},[79,5389,5390],{"class":116}," handlePaymentFailure",[79,5392,5393],{"class":89},"(invoice);\n",[79,5395,5396,5398],{"class":81,"line":703},[79,5397,5298],{"class":85},[79,5399,100],{"class":89},[79,5401,5402],{"class":81,"line":715},[79,5403,3323],{"class":89},[79,5405,5406,5409],{"class":81,"line":726},[79,5407,5408],{"class":85},"        default",[79,5410,5249],{"class":89},[79,5412,5413,5416,5419,5421,5424,5427,5429,5432,5434],{"class":81,"line":731},[79,5414,5415],{"class":89},"          console.",[79,5417,5418],{"class":116},"log",[79,5420,269],{"class":89},[79,5422,5423],{"class":96},"`[Stripe Webhook] Пропущено событие: ${",[79,5425,5426],{"class":89},"event",[79,5428,60],{"class":96},[79,5430,5431],{"class":89},"type",[79,5433,510],{"class":96},[79,5435,340],{"class":89},[79,5437,5438],{"class":81,"line":736},[79,5439,2841],{"class":89},[79,5441,5442],{"class":81,"line":742},[79,5443,107],{"emptyLinePlaceholder":106},[79,5445,5446,5449,5451,5454,5456],{"class":81,"line":750},[79,5447,5448],{"class":89},"      res.",[79,5450,2583],{"class":116},[79,5452,5453],{"class":89},"({ received: ",[79,5455,817],{"class":133},[79,5457,5458],{"class":89}," });\n",[79,5460,5461,5463,5465,5467,5469,5471,5473],{"class":81,"line":772},[79,5462,5147],{"class":89},[79,5464,829],{"class":85},[79,5466,355],{"class":89},[79,5468,5154],{"class":126},[79,5470,130],{"class":85},[79,5472,1043],{"class":133},[79,5474,5161],{"class":89},[79,5476,5477,5479,5481,5483,5486,5488,5490,5492,5494],{"class":81,"line":810},[79,5478,5166],{"class":89},[79,5480,5169],{"class":116},[79,5482,269],{"class":89},[79,5484,5485],{"class":96},"`[Webhook Process Error] Ошибка обработки события: ${",[79,5487,5154],{"class":89},[79,5489,60],{"class":96},[79,5491,5181],{"class":89},[79,5493,510],{"class":96},[79,5495,340],{"class":89},[79,5497,5498,5500,5502,5504,5507,5509,5511,5513,5516],{"class":81,"line":823},[79,5499,5448],{"class":89},[79,5501,5066],{"class":116},[79,5503,269],{"class":89},[79,5505,5506],{"class":133},"500",[79,5508,441],{"class":89},[79,5510,5076],{"class":116},[79,5512,269],{"class":89},[79,5514,5515],{"class":96},"'Internal Server Error'",[79,5517,340],{"class":89},[79,5519,5520],{"class":81,"line":835},[79,5521,1190],{"class":89},[79,5523,5524],{"class":81,"line":846},[79,5525,382],{"class":89},[79,5527,5528],{"class":81,"line":851},[79,5529,340],{"class":89},[79,5531,5533],{"class":81,"line":5532},62,[79,5534,107],{"emptyLinePlaceholder":106},[79,5536,5538,5541,5543,5545,5547,5550,5552,5554,5556,5558],{"class":81,"line":5537},63,[79,5539,5540],{"class":85},"async",[79,5542,263],{"class":85},[79,5544,5290],{"class":116},[79,5546,269],{"class":89},[79,5548,5549],{"class":126},"sub",[79,5551,130],{"class":85},[79,5553,4050],{"class":116},[79,5555,60],{"class":89},[79,5557,5280],{"class":116},[79,5559,5161],{"class":89},[79,5561,5563,5565,5568,5570],{"class":81,"line":5562},64,[79,5564,301],{"class":85},[79,5566,5567],{"class":133}," tenantId",[79,5569,307],{"class":85},[79,5571,5572],{"class":89}," sub.metadata.tenantId;\n",[79,5574,5576,5578,5580,5582],{"class":81,"line":5575},65,[79,5577,352],{"class":85},[79,5579,355],{"class":89},[79,5581,358],{"class":85},[79,5583,5584],{"class":89},"tenantId) {\n",[79,5586,5588,5590,5592,5594,5596,5599,5601,5603,5606,5609],{"class":81,"line":5587},66,[79,5589,4333],{"class":85},[79,5591,310],{"class":85},[79,5593,4338],{"class":116},[79,5595,269],{"class":89},[79,5597,5598],{"class":96},"`В метаданных подписки ${",[79,5600,5549],{"class":89},[79,5602,60],{"class":96},[79,5604,5605],{"class":89},"id",[79,5607,5608],{"class":96},"} отсутствует ID клиента (tenantId).`",[79,5610,340],{"class":89},[79,5612,5614],{"class":81,"line":5613},67,[79,5615,382],{"class":89},[79,5617,5619],{"class":81,"line":5618},68,[79,5620,107],{"emptyLinePlaceholder":106},[79,5622,5624,5626,5629,5631,5634],{"class":81,"line":5623},69,[79,5625,301],{"class":85},[79,5627,5628],{"class":133}," status",[79,5630,307],{"class":85},[79,5632,5633],{"class":89}," sub.status; ",[79,5635,5636],{"class":393},"\u002F\u002F 'active', 'trialing', 'past_due' и др.\n",[79,5638,5640,5642,5645,5647,5650,5653],{"class":81,"line":5639},70,[79,5641,301],{"class":85},[79,5643,5644],{"class":133}," priceId",[79,5646,307],{"class":85},[79,5648,5649],{"class":89}," sub.items.data[",[79,5651,5652],{"class":133},"0",[79,5654,5655],{"class":89},"].price.id;\n",[79,5657,5659,5661,5664,5666,5668,5671,5674,5677,5680],{"class":81,"line":5658},71,[79,5660,301],{"class":85},[79,5662,5663],{"class":133}," currentPeriodEnd",[79,5665,307],{"class":85},[79,5667,310],{"class":85},[79,5669,5670],{"class":116}," Date",[79,5672,5673],{"class":89},"(sub.current_period_end ",[79,5675,5676],{"class":85},"*",[79,5678,5679],{"class":133}," 1000",[79,5681,340],{"class":89},[79,5683,5685],{"class":81,"line":5684},72,[79,5686,107],{"emptyLinePlaceholder":106},[79,5688,5690],{"class":81,"line":5689},73,[79,5691,5692],{"class":393},"  \u002F\u002F Синхронизируем статус в нашей базе данных\n",[79,5694,5696,5699,5702,5705],{"class":81,"line":5695},74,[79,5697,5698],{"class":85},"  await",[79,5700,5701],{"class":89}," db.",[79,5703,5704],{"class":116},"query",[79,5706,4957],{"class":89},[79,5708,5710],{"class":81,"line":5709},75,[79,5711,5712],{"class":96},"    `UPDATE tenants \n",[79,5714,5716],{"class":81,"line":5715},76,[79,5717,5718],{"class":96},"     SET subscription_status = $1, stripe_price_id = $2, subscription_period_end = $3\n",[79,5720,5722,5725],{"class":81,"line":5721},77,[79,5723,5724],{"class":96},"     WHERE id = $4`",[79,5726,2604],{"class":89},[79,5728,5730],{"class":81,"line":5729},78,[79,5731,5732],{"class":89},"    [status, priceId, currentPeriodEnd, tenantId]\n",[79,5734,5736],{"class":81,"line":5735},79,[79,5737,5738],{"class":89},"  );\n",[79,5740,5742],{"class":81,"line":5741},80,[79,5743,107],{"emptyLinePlaceholder":106},[79,5745,5747,5750,5752,5754,5757,5760,5763,5765,5767],{"class":81,"line":5746},81,[79,5748,5749],{"class":89},"  console.",[79,5751,5418],{"class":116},[79,5753,269],{"class":89},[79,5755,5756],{"class":96},"`[Billing Sync] Статус клиента ${",[79,5758,5759],{"class":89},"tenantId",[79,5761,5762],{"class":96},"} обновлен на: ${",[79,5764,5066],{"class":89},[79,5766,510],{"class":96},[79,5768,340],{"class":89},[79,5770,5772],{"class":81,"line":5771},82,[79,5773,205],{"class":89},[79,5775,5777],{"class":81,"line":5776},83,[79,5778,107],{"emptyLinePlaceholder":106},[79,5780,5782,5784,5786,5788,5790,5792,5794,5796,5798,5800],{"class":81,"line":5781},84,[79,5783,5540],{"class":85},[79,5785,263],{"class":85},[79,5787,5340],{"class":116},[79,5789,269],{"class":89},[79,5791,5549],{"class":126},[79,5793,130],{"class":85},[79,5795,4050],{"class":116},[79,5797,60],{"class":89},[79,5799,5280],{"class":116},[79,5801,5161],{"class":89},[79,5803,5805,5807,5809,5811],{"class":81,"line":5804},85,[79,5806,301],{"class":85},[79,5808,5567],{"class":133},[79,5810,307],{"class":85},[79,5812,5572],{"class":89},[79,5814,5816,5818,5820,5822,5825,5828],{"class":81,"line":5815},86,[79,5817,352],{"class":85},[79,5819,355],{"class":89},[79,5821,358],{"class":85},[79,5823,5824],{"class":89},"tenantId) ",[79,5826,5827],{"class":85},"return",[79,5829,100],{"class":89},[79,5831,5833],{"class":81,"line":5832},87,[79,5834,107],{"emptyLinePlaceholder":106},[79,5836,5838,5840,5842,5844],{"class":81,"line":5837},88,[79,5839,5698],{"class":85},[79,5841,5701],{"class":89},[79,5843,5704],{"class":116},[79,5845,4957],{"class":89},[79,5847,5849],{"class":81,"line":5848},89,[79,5850,5712],{"class":96},[79,5852,5854],{"class":81,"line":5853},90,[79,5855,5856],{"class":96},"     SET subscription_status = 'canceled', stripe_price_id = NULL\n",[79,5858,5860,5863],{"class":81,"line":5859},91,[79,5861,5862],{"class":96},"     WHERE id = $1`",[79,5864,2604],{"class":89},[79,5866,5868],{"class":81,"line":5867},92,[79,5869,5870],{"class":89},"    [tenantId]\n",[79,5872,5874],{"class":81,"line":5873},93,[79,5875,5738],{"class":89},[79,5877,5879],{"class":81,"line":5878},94,[79,5880,107],{"emptyLinePlaceholder":106},[79,5882,5884,5886,5888,5890,5893,5895,5898],{"class":81,"line":5883},95,[79,5885,5749],{"class":89},[79,5887,5418],{"class":116},[79,5889,269],{"class":89},[79,5891,5892],{"class":96},"`[Billing Sync] Подписка клиента ${",[79,5894,5759],{"class":89},[79,5896,5897],{"class":96},"} аннулирована.`",[79,5899,340],{"class":89},[79,5901,5903],{"class":81,"line":5902},96,[79,5904,205],{"class":89},[79,5906,5908],{"class":81,"line":5907},97,[79,5909,107],{"emptyLinePlaceholder":106},[79,5911,5913,5915,5917,5919,5921,5924,5926,5928,5930,5932],{"class":81,"line":5912},98,[79,5914,5540],{"class":85},[79,5916,263],{"class":85},[79,5918,5390],{"class":116},[79,5920,269],{"class":89},[79,5922,5923],{"class":126},"invoice",[79,5925,130],{"class":85},[79,5927,4050],{"class":116},[79,5929,60],{"class":89},[79,5931,5381],{"class":116},[79,5933,5161],{"class":89},[79,5935,5937,5939,5942,5944,5947,5949,5951],{"class":81,"line":5936},99,[79,5938,301],{"class":85},[79,5940,5941],{"class":133}," customerId",[79,5943,307],{"class":85},[79,5945,5946],{"class":89}," invoice.customer ",[79,5948,1040],{"class":85},[79,5950,147],{"class":133},[79,5952,100],{"class":89},[79,5954,5956],{"class":81,"line":5955},100,[79,5957,5958],{"class":393},"  \u002F\u002F Находим пользователя по customerId и отправляем предупреждающее письмо\n",[79,5960,5962,5964,5967,5969,5972,5975,5977],{"class":81,"line":5961},101,[79,5963,5749],{"class":89},[79,5965,5966],{"class":116},"warn",[79,5968,269],{"class":89},[79,5970,5971],{"class":96},"`[Billing Alert] Ошибка оплаты счета для клиента ${",[79,5973,5974],{"class":89},"customerId",[79,5976,510],{"class":96},[79,5978,340],{"class":89},[79,5980,5982],{"class":81,"line":5981},102,[79,5983,205],{"class":89},[26,5985,5987],{"id":5986},"_3-рекомендации-по-интеграции-биллинга-в-saas","3. Рекомендации по интеграции биллинга в SaaS",[1229,5989,5990,5996,6006],{},[1232,5991,5992,5995],{},[15,5993,5994],{},"Stripe Checkout и Customer Portal спасают недели работы",": Не тратьте ресурсы на создание дашбордов привязки карт, просмотра истории инвойсов и отмены услуг. Готовые решения Stripe безопасны, соответствуют PCI DSS и переведены на десятки языков.",[1232,5997,5998,6001,6002,6005],{},[15,5999,6000],{},"Используйте Metadata",": Всегда передавайте ID сущностей вашей базы данных в поле ",[42,6003,6004],{},"metadata"," при создании платежных сессий. Это единственный надежный способ сопоставить событие вебхука с конкретной строкой в вашей БД.",[1232,6007,6008,6011],{},[15,6009,6010],{},"Защита от дублирования (Идемпотентность)",": Вебхуки могут приходить повторно. Убедитесь, что ваш обработчик проверяет, не был ли статус уже записан, чтобы исключить лишние операции отправки писем или пересчета лимитов.",[11,6013,3957,6014,6016,6017,60],{},[1270,6015,1273],{"href":1272}," мы внедрили именно эту архитектуру верификации вебхуков для безопасного управления подписками. Мы также совместили её с микротранзакциями Telegram Stars, о чем вы можете прочитать в моей статье ",[1270,6018,6020],{"href":6019},"\u002Fru\u002Fblog\u002Ftelegram-web-apps-sells","Разработка SaaS внутри Telegram Web Apps",[26,6022,1290],{"id":1289},[1292,6024,6025,6033,6041],{},[1232,6026,6027,6032],{},[1270,6028,6031],{"href":6029,"rel":6030},"https:\u002F\u002Fdocs.stripe.com\u002Fwebhooks",[1300],"Stripe Webhooks"," — доставка, подпись и повторные попытки событий",[1232,6034,6035,6040],{},[1270,6036,6039],{"href":6037,"rel":6038},"https:\u002F\u002Fdocs.stripe.com\u002Fbilling\u002Fsubscriptions\u002Foverview",[1300],"Stripe: жизненный цикл подписок"," — все статусы из схемы выше",[1232,6042,6043,6048],{},[1270,6044,6047],{"href":6045,"rel":6046},"https:\u002F\u002Fdocs.stripe.com\u002Fstripe-cli",[1300],"Stripe CLI"," — локальное тестирование вебхуков",[11,6050,6051,6052,60],{},"Профессиональная платежная архитектура защищает доходы вашего SaaS-бизнеса и гарантирует высокий уровень лояльности платящих клиентов. Чтобы узнать, как биллинг встраивается в общий цикл быстрого запуска стартапа, ознакомьтесь с моим руководством ",[1270,6053,6054],{"href":3771},"Как собрать B2B SaaS за 30 дней",[11,6056,6057,6058,1321,6060,6062],{},"Если вы планируете настроить прием платежей на своем проекте или провести рефакторинг текущей платежной системы, изучите мою услугу ",[1270,6059,4716],{"href":3778},[1270,6061,1325],{"href":1324},", чтобы разработать надежный биллинг.",[1328,6064,6065],{},"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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}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":6067},[6068,6069,6070,6071],{"id":4767,"depth":103,"text":4768},{"id":4810,"depth":103,"text":4811},{"id":5986,"depth":103,"text":5987},{"id":1289,"depth":103,"text":1290},"2026-06-14","Подробное техническое руководство по интеграции рекуррентных платежей Stripe в B2B SaaS-приложения, валидации подписи вебхуков и управлению льготными периодами.",[6075,6078,6081],{"q":6076,"a":6077},"Почему нельзя выдавать доступ сразу после редиректа с оплаты?","Редирект на success-страницу не гарантирует, что деньги списаны: пользователь мог закрыть вкладку, а платёж — не пройти антифрод. Единственный источник истины — вебхуки Stripe: доступ выдаётся по событию checkout.session.completed, а продлевается по invoice.paid.",{"q":6079,"a":6080},"Что делать, если вебхук от Stripe не дошёл?","Stripe автоматически повторяет доставку событий до трёх суток, поэтому обработчик обязан быть идемпотентным — повторное событие не должно дублировать письма или продления. Для страховки добавляют фоновую сверку статусов подписок по расписанию.",{"q":6082,"a":6083},"Как тестировать вебхуки локально?","Через Stripe CLI: команда stripe listen пробрасывает события на localhost, а stripe trigger генерирует тестовые события любого типа — от успешной оплаты до провала списания. Это позволяет отладить все ветки обработчика до продакшена.","\u002Fimages\u002Fblog\u002Fblog_stripe_billing.jpg",{},"\u002Fblog\u002Fru\u002Fsaas-stripe-billing-integration",{"title":4753,"description":6073},"blog\u002Fru\u002Fsaas-stripe-billing-integration",[1364,6090,1365,6091,6092],"Платежи","API","Stripe","VV1Hk94TSww6REieAjuXB7iyyNloDJZ0IN_UJQh4rt8",{"id":6095,"title":6096,"body":6097,"date":6783,"description":6784,"extension":1344,"faq":6785,"image":6795,"lang":1356,"meta":6796,"navigation":106,"path":6797,"published":106,"readTime":213,"seo":6798,"stem":6799,"tags":6800,"updated":1342,"__hash__":6804},"blog\u002Fblog\u002Fru\u002Fsaas-multitenancy-databases.md","Архитектура SaaS: Single-Tenant против Multi-Tenant баз данных",{"type":8,"value":6098,"toc":6772},[6099,6102,6105,6109,6115,6119,6122,6262,6266,6273,6280,6284,6287,6392,6396,6406,6426,6430,6433,6691,6695,6721,6727,6729,6755,6760,6769],[11,6100,6101],{},"Выбор архитектуры базы данных для B2B SaaS — это одно из самых ответственных решений. Оно напрямую влияет на стоимость серверов, скорость разработки новых функций, сложность проведения миграций и соответствие стандартам защиты данных (таким как GDPR, HIPAA или 152-ФЗ).",[11,6103,6104],{},"Архитектурная ошибка на старте обойдется крайне дорого при попытке исправить ее в будущем. В этой статье мы подробно проанализируем три основных паттерна организации баз данных, сравним их ключевые характеристики и напишем безопасную реализацию разделения данных с помощью PostgreSQL Row-Level Security (RLS).",[26,6106,6108],{"id":6107},"три-парадигмы-multi-tenant-архитектуры","Три парадигмы Multi-Tenant архитектуры",[34,6110,6113],{"className":6111,"code":6112,"language":39,"meta":40},[37],"+-----------------------+   +-----------------------+   +-----------------------+\n|  Паттерн А: Раздельные|   |   Паттерн Б: Раздельные|   |   Паттерн В: Общая    |\n|      базы данных      |   |        схемы          |   |     база и таблицы    |\n+-----------------------+   +-----------------------+   +-----------------------+\n|  [DB 1]      [DB 2]   |   |  [База Данных]        |   |  [База Данных]        |\n|  Клиент А    Клиент Б |   |   ├── Схема Клиент А  |   |   └── [Таблица]       |\n|                       |   |   └── Схема Клиент Б  |   |        ├── Строка кл.А|\n| (Макс. изоляция\u002Fцена) |   | (Средняя изоляция)    |   |        └── Строка кл.Б|\n+-----------------------+   +-----------------------+   +-----------------------+\n",[42,6114,6112],{"__ignoreMap":40},[26,6116,6118],{"id":6117},"_1-сравнение-архитектурных-подходов","1. Сравнение архитектурных подходов",[11,6120,6121],{},"Давайте сопоставим все три паттерна по критически важным для бизнеса и разработки критериям:",[2415,6123,6124,6140],{},[2418,6125,6126],{},[2421,6127,6128,6131,6134,6137],{},[2424,6129,6130],{"align":2426},"Критерий",[2424,6132,6133],{"align":2426},"Паттерн А: Выделенные БД",[2424,6135,6136],{"align":2426},"Паттерн Б: Выделенные схемы",[2424,6138,6139],{"align":2426},"Паттерн В: Общая таблица + RLS",[2435,6141,6142,6167,6190,6214,6239],{},[2421,6143,6144,6149,6155,6161],{},[2440,6145,6146],{"align":2426},[15,6147,6148],{},"Изоляция данных",[2440,6150,6151,6154],{"align":2426},[15,6152,6153],{},"Максимальная",": Нулевой риск случайного перекрестного доступа.",[2440,6156,6157,6160],{"align":2426},[15,6158,6159],{},"Высокая",": Разделение на уровне пространств имен СУБД.",[2440,6162,6163,6166],{"align":2426},[15,6164,6165],{},"Средняя",": Риск утечки при ошибках в SQL-запросах или политиках.",[2421,6168,6169,6174,6179,6184],{},[2440,6170,6171],{"align":2426},[15,6172,6173],{},"Стоимость серверов",[2440,6175,6176,6178],{"align":2426},[15,6177,6153],{},": Огромные накладные расходы на ресурсы и пулы соединений.",[2440,6180,6181,6183],{"align":2426},[15,6182,6165],{},": Общее дисковое пространство, но нагрузка на кэш каталогов.",[2440,6185,6186,6189],{"align":2426},[15,6187,6188],{},"Минимальная",": Максимально эффективное использование железа.",[2421,6191,6192,6197,6203,6208],{},[2440,6193,6194],{"align":2426},[15,6195,6196],{},"Сложность миграций",[2440,6198,6199,6202],{"align":2426},[15,6200,6201],{},"Очень высокая",": Требуется последовательно обновлять сотни баз.",[2440,6204,6205,6207],{"align":2426},[15,6206,6159],{},": Циклическое применение миграций по всем схемам.",[2440,6209,6210,6213],{"align":2426},[15,6211,6212],{},"Простая",": Стандартный накат одной миграции на всю базу.",[2421,6215,6216,6221,6227,6233],{},[2440,6217,6218],{"align":2426},[15,6219,6220],{},"Пулы соединений",[2440,6222,6223,6226],{"align":2426},[15,6224,6225],{},"Сложно",": Отдельный пул под каждого клиента.",[2440,6228,6229,6232],{"align":2426},[15,6230,6231],{},"Средне",": Общий пул, но с постоянным переключением схем.",[2440,6234,6235,6238],{"align":2426},[15,6236,6237],{},"Просто",": Единый стандартный пул соединений для всех.",[2421,6240,6241,6246,6252,6257],{},[2440,6242,6243],{"align":2426},[15,6244,6245],{},"Бэкап и Восстановление",[2440,6247,6248,6251],{"align":2426},[15,6249,6250],{},"Идеально",": Можно восстановить только одного клиента из резервной копии.",[2440,6253,6254,6256],{"align":2426},[15,6255,6231],{},": Требуется выборочный дамп отдельных схем (pg_dump).",[2440,6258,6259,6261],{"align":2426},[15,6260,6225],{},": Восстановление одного клиента требует выборки данных.",[26,6263,6265],{"id":6264},"_2-реализация-row-level-security-rls-в-postgresql","2. Реализация Row-Level Security (RLS) в PostgreSQL",[11,6267,6268,6269,6272],{},"Если вы выбираете наиболее экономичный и простой в поддержке ",[15,6270,6271],{},"Паттерн В"," (Общие таблицы), безопасность данных необходимо гарантировать на уровне ядра базы данных.",[11,6274,6275,6276,6279],{},"Механизм ",[15,6277,6278],{},"Row-Level Security (RLS)"," в PostgreSQL позволяет автоматически фильтровать строки при любых запросах в зависимости от переменных текущей сессии.",[862,6281,6283],{"id":6282},"шаг-1-создание-таблиц-и-включение-rls","Шаг 1: Создание таблиц и включение RLS",[11,6285,6286],{},"Спроектируем таблицы с поддержкой тенантов. Обратите внимание на обязательное индексирование внешних ключей.",[34,6288,6292],{"className":6289,"code":6290,"language":6291,"meta":40,"style":40},"language-sql shiki shiki-themes github-dark","-- Таблица клиентов (тенантов)\nCREATE TABLE tenants (\n  id SERIAL PRIMARY KEY,\n  company_name VARCHAR(255) NOT NULL,\n  created_at TIMESTAMP DEFAULT NOW()\n);\n\n-- Таблица проектов с привязкой к клиенту\nCREATE TABLE projects (\n  id SERIAL PRIMARY KEY,\n  tenant_id INT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,\n  name VARCHAR(255) NOT NULL,\n  status VARCHAR(50) DEFAULT 'draft',\n  created_at TIMESTAMP DEFAULT NOW()\n);\n\n-- Индексируем tenant_id (Критически важно для скорости запросов!)\nCREATE INDEX idx_projects_tenant ON projects(tenant_id);\n\n-- Включаем защиту строк RLS для таблицы проектов\nALTER TABLE projects ENABLE ROW LEVEL SECURITY;\n","sql",[42,6293,6294,6299,6304,6309,6314,6319,6323,6327,6332,6337,6341,6346,6351,6356,6360,6364,6368,6373,6378,6382,6387],{"__ignoreMap":40},[79,6295,6296],{"class":81,"line":82},[79,6297,6298],{},"-- Таблица клиентов (тенантов)\n",[79,6300,6301],{"class":81,"line":103},[79,6302,6303],{},"CREATE TABLE tenants (\n",[79,6305,6306],{"class":81,"line":110},[79,6307,6308],{},"  id SERIAL PRIMARY KEY,\n",[79,6310,6311],{"class":81,"line":123},[79,6312,6313],{},"  company_name VARCHAR(255) NOT NULL,\n",[79,6315,6316],{"class":81,"line":139},[79,6317,6318],{},"  created_at TIMESTAMP DEFAULT NOW()\n",[79,6320,6321],{"class":81,"line":152},[79,6322,340],{},[79,6324,6325],{"class":81,"line":165},[79,6326,107],{"emptyLinePlaceholder":106},[79,6328,6329],{"class":81,"line":177},[79,6330,6331],{},"-- Таблица проектов с привязкой к клиенту\n",[79,6333,6334],{"class":81,"line":189},[79,6335,6336],{},"CREATE TABLE projects (\n",[79,6338,6339],{"class":81,"line":202},[79,6340,6308],{},[79,6342,6343],{"class":81,"line":208},[79,6344,6345],{},"  tenant_id INT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,\n",[79,6347,6348],{"class":81,"line":213},[79,6349,6350],{},"  name VARCHAR(255) NOT NULL,\n",[79,6352,6353],{"class":81,"line":223},[79,6354,6355],{},"  status VARCHAR(50) DEFAULT 'draft',\n",[79,6357,6358],{"class":81,"line":235},[79,6359,6318],{},[79,6361,6362],{"class":81,"line":247},[79,6363,340],{},[79,6365,6366],{"class":81,"line":252},[79,6367,107],{"emptyLinePlaceholder":106},[79,6369,6370],{"class":81,"line":257},[79,6371,6372],{},"-- Индексируем tenant_id (Критически важно для скорости запросов!)\n",[79,6374,6375],{"class":81,"line":298},[79,6376,6377],{},"CREATE INDEX idx_projects_tenant ON projects(tenant_id);\n",[79,6379,6380],{"class":81,"line":319},[79,6381,107],{"emptyLinePlaceholder":106},[79,6383,6384],{"class":81,"line":343},[79,6385,6386],{},"-- Включаем защиту строк RLS для таблицы проектов\n",[79,6388,6389],{"class":81,"line":349},[79,6390,6391],{},"ALTER TABLE projects ENABLE ROW LEVEL SECURITY;\n",[862,6393,6395],{"id":6394},"шаг-2-создание-политики-безопасности","Шаг 2: Создание политики безопасности",[11,6397,6398,6399,6402,6403,60],{},"Настроим политику, которая сопоставляет поле ",[42,6400,6401],{},"tenant_id"," с переменной сессии ",[42,6404,6405],{},"app.current_tenant_id",[34,6407,6409],{"className":6289,"code":6408,"language":6291,"meta":40,"style":40},"CREATE POLICY tenant_isolation_policy ON projects\n  AS ASYMMETRIC\n  USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::integer);\n",[42,6410,6411,6416,6421],{"__ignoreMap":40},[79,6412,6413],{"class":81,"line":82},[79,6414,6415],{},"CREATE POLICY tenant_isolation_policy ON projects\n",[79,6417,6418],{"class":81,"line":103},[79,6419,6420],{},"  AS ASYMMETRIC\n",[79,6422,6423],{"class":81,"line":110},[79,6424,6425],{},"  USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::integer);\n",[862,6427,6429],{"id":6428},"шаг-3-выполнение-запросов-из-кода-приложения-nodejs","Шаг 3: Выполнение запросов из кода приложения (Node.js)",[11,6431,6432],{},"Бэкенд-контроллер (например, в Nuxt server или Express) выполняет операции внутри SQL-транзакции, передавая ID текущего пользователя перед выполнением запроса.",[34,6434,6436],{"className":73,"code":6435,"language":75,"meta":40,"style":40},"import { Client } from 'pg';\n\nasync function fetchTenantProjects(tenantId: number): Promise\u003Cany[]> {\n  const client = new Client({ connectionString: process.env.DATABASE_URL });\n  await client.connect();\n\n  try {\n    \u002F\u002F Начинаем транзакцию\n    await client.query('BEGIN');\n\n    \u002F\u002F Устанавливаем ID текущего клиента для этой транзакции\n    await client.query(`SET LOCAL app.current_tenant_id = $1`, [tenantId]);\n\n    \u002F\u002F Даже без указания WHERE tenant_id = X база данных вернет только нужные строки\n    const result = await client.query('SELECT * FROM projects');\n\n    await client.query('COMMIT');\n    return result.rows;\n  } catch (error) {\n    await client.query('ROLLBACK');\n    throw error;\n  } finally {\n    await client.end();\n  }\n}\n",[42,6437,6438,6452,6456,6486,6508,6520,6524,6530,6535,6551,6555,6560,6576,6580,6585,6607,6611,6626,6633,6641,6656,6663,6672,6683,6687],{"__ignoreMap":40},[79,6439,6440,6442,6445,6447,6450],{"class":81,"line":82},[79,6441,86],{"class":85},[79,6443,6444],{"class":89}," { Client } ",[79,6446,93],{"class":85},[79,6448,6449],{"class":96}," 'pg'",[79,6451,100],{"class":89},[79,6453,6454],{"class":81,"line":103},[79,6455,107],{"emptyLinePlaceholder":106},[79,6457,6458,6460,6462,6465,6467,6469,6471,6473,6475,6477,6479,6481,6483],{"class":81,"line":110},[79,6459,5540],{"class":85},[79,6461,263],{"class":85},[79,6463,6464],{"class":116}," fetchTenantProjects",[79,6466,269],{"class":89},[79,6468,5759],{"class":126},[79,6470,130],{"class":85},[79,6472,134],{"class":133},[79,6474,289],{"class":89},[79,6476,130],{"class":85},[79,6478,4160],{"class":116},[79,6480,997],{"class":89},[79,6482,1000],{"class":133},[79,6484,6485],{"class":89},"[]> {\n",[79,6487,6488,6490,6493,6495,6497,6500,6503,6506],{"class":81,"line":123},[79,6489,301],{"class":85},[79,6491,6492],{"class":133}," client",[79,6494,307],{"class":85},[79,6496,310],{"class":85},[79,6498,6499],{"class":116}," Client",[79,6501,6502],{"class":89},"({ connectionString: process.env.",[79,6504,6505],{"class":133},"DATABASE_URL",[79,6507,5458],{"class":89},[79,6509,6510,6512,6515,6518],{"class":81,"line":139},[79,6511,5698],{"class":85},[79,6513,6514],{"class":89}," client.",[79,6516,6517],{"class":116},"connect",[79,6519,447],{"class":89},[79,6521,6522],{"class":81,"line":152},[79,6523,107],{"emptyLinePlaceholder":106},[79,6525,6526,6528],{"class":81,"line":165},[79,6527,745],{"class":85},[79,6529,120],{"class":89},[79,6531,6532],{"class":81,"line":177},[79,6533,6534],{"class":393},"    \u002F\u002F Начинаем транзакцию\n",[79,6536,6537,6540,6542,6544,6546,6549],{"class":81,"line":189},[79,6538,6539],{"class":85},"    await",[79,6541,6514],{"class":89},[79,6543,5704],{"class":116},[79,6545,269],{"class":89},[79,6547,6548],{"class":96},"'BEGIN'",[79,6550,340],{"class":89},[79,6552,6553],{"class":81,"line":202},[79,6554,107],{"emptyLinePlaceholder":106},[79,6556,6557],{"class":81,"line":208},[79,6558,6559],{"class":393},"    \u002F\u002F Устанавливаем ID текущего клиента для этой транзакции\n",[79,6561,6562,6564,6566,6568,6570,6573],{"class":81,"line":213},[79,6563,6539],{"class":85},[79,6565,6514],{"class":89},[79,6567,5704],{"class":116},[79,6569,269],{"class":89},[79,6571,6572],{"class":96},"`SET LOCAL app.current_tenant_id = $1`",[79,6574,6575],{"class":89},", [tenantId]);\n",[79,6577,6578],{"class":81,"line":223},[79,6579,107],{"emptyLinePlaceholder":106},[79,6581,6582],{"class":81,"line":235},[79,6583,6584],{"class":393},"    \u002F\u002F Даже без указания WHERE tenant_id = X база данных вернет только нужные строки\n",[79,6586,6587,6589,6592,6594,6596,6598,6600,6602,6605],{"class":81,"line":247},[79,6588,753],{"class":85},[79,6590,6591],{"class":133}," result",[79,6593,307],{"class":85},[79,6595,4180],{"class":85},[79,6597,6514],{"class":89},[79,6599,5704],{"class":116},[79,6601,269],{"class":89},[79,6603,6604],{"class":96},"'SELECT * FROM projects'",[79,6606,340],{"class":89},[79,6608,6609],{"class":81,"line":252},[79,6610,107],{"emptyLinePlaceholder":106},[79,6612,6613,6615,6617,6619,6621,6624],{"class":81,"line":257},[79,6614,6539],{"class":85},[79,6616,6514],{"class":89},[79,6618,5704],{"class":116},[79,6620,269],{"class":89},[79,6622,6623],{"class":96},"'COMMIT'",[79,6625,340],{"class":89},[79,6627,6628,6630],{"class":81,"line":298},[79,6629,367],{"class":85},[79,6631,6632],{"class":89}," result.rows;\n",[79,6634,6635,6637,6639],{"class":81,"line":319},[79,6636,826],{"class":89},[79,6638,829],{"class":85},[79,6640,832],{"class":89},[79,6642,6643,6645,6647,6649,6651,6654],{"class":81,"line":343},[79,6644,6539],{"class":85},[79,6646,6514],{"class":89},[79,6648,5704],{"class":116},[79,6650,269],{"class":89},[79,6652,6653],{"class":96},"'ROLLBACK'",[79,6655,340],{"class":89},[79,6657,6658,6660],{"class":81,"line":349},[79,6659,4333],{"class":85},[79,6661,6662],{"class":89}," error;\n",[79,6664,6665,6667,6670],{"class":81,"line":364},[79,6666,826],{"class":89},[79,6668,6669],{"class":85},"finally",[79,6671,120],{"class":89},[79,6673,6674,6676,6678,6681],{"class":81,"line":379},[79,6675,6539],{"class":85},[79,6677,6514],{"class":89},[79,6679,6680],{"class":116},"end",[79,6682,447],{"class":89},[79,6684,6685],{"class":81,"line":385},[79,6686,382],{"class":89},[79,6688,6689],{"class":81,"line":390},[79,6690,205],{"class":89},[26,6692,6694],{"id":6693},"_3-рекомендации-по-масштабированию-и-поддержке","3. Рекомендации по масштабированию и поддержке",[1229,6696,6697,6703,6712],{},[1232,6698,6699,6702],{},[15,6700,6701],{},"Всегда покрывайте автотестами",": Напишите интеграционные тесты, которые пытаются прочитать проекты Клиента Б через сессию Клиента А. Тест должен завершаться ошибкой доступа или возвращать пустой массив.",[1232,6704,6705,6708,6709,6711],{},[15,6706,6707],{},"Заранее готовьтесь к экспорту",": Некоторые крупные корпоративные клиенты со временем могут потребовать выделенный сервер. Ваша структура должна позволять быстро отфильтровать все связанные данные по ",[42,6710,6401],{}," и перенести их на отдельный инстанс.",[1232,6713,6714,6717,6718,60],{},[15,6715,6716],{},"Не забывайте про композитные индексы",": Если вы ищете проекты по дате создания внутри конкретного клиента, создайте композитный индекс ",[42,6719,6720],{},"(tenant_id, created_at)",[11,6722,6723,6724,6726],{},"Например, на языковом портале ",[1270,6725,2863],{"href":2862}," мы успешно развернули изоляцию на уровне строк (Паттерн В), используя индексированные схемы тенантов, что позволило удерживать расходы на СУБД на минимальном уровне при обслуживании тысяч активных студентов.",[26,6728,1290],{"id":1289},[1292,6730,6731,6739,6747],{},[1232,6732,6733,6738],{},[1270,6734,6737],{"href":6735,"rel":6736},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fddl-rowsecurity.html",[1300],"PostgreSQL: Row Security Policies"," — официальная документация по RLS",[1232,6740,6741,6746],{},[1270,6742,6745],{"href":6743,"rel":6744},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fddl-schemas.html",[1300],"PostgreSQL: Schemas"," — логические схемы для Паттерна Б",[1232,6748,6749,6754],{},[1270,6750,6753],{"href":6751,"rel":6752},"https:\u002F\u002Fgdpr.eu\u002F",[1300],"GDPR.eu"," — требования к изоляции и переносимости персональных данных",[11,6756,6757,6758,60],{},"Правильный выбор архитектуры базы данных обеспечивает баланс между бюджетом стартапа и строгими требованиями безопасности. Использование PostgreSQL RLS позволяет создать сверхдешевую, быструю в разработке и при этом надежно защищенную SaaS-архитектуру. Чтобы узнать, как эти решения интегрируются в общий процесс запуска продукта, изучите мое руководство ",[1270,6759,6054],{"href":3771},[11,6761,6762,6763,6765,6766,60],{},"Если вы проектируете архитектуру базы данных для своего SaaS-сервиса и хотите получить независимый экспертный аудит решений, изучите мою страницу ",[1270,6764,4716],{"href":3778}," или запишитесь на детальную ",[1270,6767,6768],{"href":1324},"Консультацию по архитектуре баз данных",[1328,6770,6771],{},"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);}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}",{"title":40,"searchDepth":103,"depth":103,"links":6773},[6774,6775,6776,6781,6782],{"id":6107,"depth":103,"text":6108},{"id":6117,"depth":103,"text":6118},{"id":6264,"depth":103,"text":6265,"children":6777},[6778,6779,6780],{"id":6282,"depth":110,"text":6283},{"id":6394,"depth":110,"text":6395},{"id":6428,"depth":110,"text":6429},{"id":6693,"depth":103,"text":6694},{"id":1289,"depth":103,"text":1290},"2026-06-11","Подробное практическое руководство по проектированию масштабируемых и безопасных баз данных для B2B SaaS. Сравниваем общие таблицы, логические схемы PostgreSQL и выделенные СУБД.",[6786,6789,6792],{"q":6787,"a":6788},"Какой паттерн multi-tenancy выбрать для старта SaaS?","Для большинства B2B-стартапов — общую базу с Row-Level Security (Паттерн В): минимальные расходы на серверы, простые миграции и достаточная изоляция при правильно написанных политиках. Выделенные базы оправданы только при жёстких регуляторных требованиях клиентов.",{"q":6790,"a":6791},"Насколько безопасна изоляция через PostgreSQL RLS?","При включённых политиках RLS база сама фильтрует строки по tenant_id на уровне СУБД — даже ошибочный SQL-запрос приложения не вернёт чужие данные. Обязательное условие: интеграционные тесты, которые пытаются прочитать данные одного клиента через сессию другого.",{"q":6793,"a":6794},"Что делать, если крупный клиент потребует выделенную базу?","Проектировать экспорт заранее: если все таблицы связаны через tenant_id, данные одного клиента выгружаются стандартной фильтрацией и переносятся на отдельный инстанс без переписывания приложения.","\u002Fimages\u002Fblog\u002Fblog_saas_multitenancy.jpg",{},"\u002Fblog\u002Fru\u002Fsaas-multitenancy-databases",{"title":6096,"description":6784},"blog\u002Fru\u002Fsaas-multitenancy-databases",[1364,6801,4440,6802,6803],"БазыДанных","PostgreSQL","SQL","80jK0Ym7oNa0TkSL4kwVs3-SnMCfacKTmnM0koxKrm8",{"id":6806,"title":6807,"body":6808,"date":7461,"description":7462,"extension":1344,"faq":7463,"image":7473,"lang":1356,"meta":7474,"navigation":106,"path":7475,"published":106,"readTime":213,"seo":7476,"stem":7477,"tags":7478,"updated":1342,"__hash__":7482},"blog\u002Fblog\u002Fru\u002Fcode-audit-technical-debt.md","Чек-лист аудита кода: Как оценить техдолг перед масштабированием",{"type":8,"value":6809,"toc":7451},[6810,6813,6816,6822,6825,6829,6832,6919,6923,6927,6930,6935,7099,7103,7106,7206,7210,7235,7354,7358,7384,7388,7391,7411,7413,7436,7441,7448],[11,6811,6812],{},"Принятие легаси-проекта от другой команды, покупка готового SaaS-стартапа или подготовка работающего MVP к этапу активного масштабирования — это критические точки развития продукта. В этих сценариях ваш самый опасный невидимый враг — это технический долг.",[11,6814,6815],{},"Код, который стабильно работает на локальном компьютере одного разработчика, в условиях продакшена может содержать критические уязвимости безопасности, неоптимальные SQL-запросы без индексов или запутанную архитектуру, из-за которой внедрение мелких доработок растягивается на недели.",[11,6817,6818,6819,60],{},"Чтобы расти безопасно и не тратить бюджеты на бесконечные переписывания, необходимо провести комплексный ",[15,6820,6821],{},"технический аудит кодовой базы",[11,6823,6824],{},"В этом руководстве мы разберем технический регламент проверки проектов на Node.js\u002FTypeScript, методы устранения проблем производительности СУБД (таких как проблема N+1), проверку безопасности и планирование рефакторинга.",[26,6826,6828],{"id":6827},"_1-автоматический-сбор-метрик","1. Автоматический сбор метрик",[11,6830,6831],{},"Перед ручным анализом бизнес-логики соберите количественные метрики с помощью специализированных инструментов статического анализа. Это задаст базовую линию для команды:",[2415,6833,6834,6850],{},[2418,6835,6836],{},[2421,6837,6838,6841,6844,6847],{},[2424,6839,6840],{"align":2426},"Направление аудита",[2424,6842,6843],{"align":2426},"Инструменты",[2424,6845,6846],{"align":2426},"Целевой показатель",[2424,6848,6849],{"align":2426},"Последствия игнорирования",[2435,6851,6852,6871,6887,6903],{},[2421,6853,6854,6859,6865,6868],{},[2440,6855,6856],{"align":2426},[15,6857,6858],{},"Безопасность зависимостей",[2440,6860,6861,6864],{"align":2426},[42,6862,6863],{},"npm audit",", Snyk, Dependabot",[2440,6866,6867],{"align":2426},"0 критических уязвимостей",[2440,6869,6870],{"align":2426},"Риск выполнения стороннего кода на сервере, кража данных.",[2421,6872,6873,6878,6881,6884],{},[2440,6874,6875],{"align":2426},[15,6876,6877],{},"Линтинг и форматирование",[2440,6879,6880],{"align":2426},"Biome, ESLint, Prettier",[2440,6882,6883],{"align":2426},"0 ошибок",[2440,6885,6886],{"align":2426},"Путаница в синтаксисе, скрытые баги в замыканиях, конфликты слияния.",[2421,6888,6889,6894,6897,6900],{},[2440,6890,6891],{"align":2426},[15,6892,6893],{},"Покрытие тестами",[2440,6895,6896],{"align":2426},"Vitest, Jest, Playwright",[2440,6898,6899],{"align":2426},"> 70% покрытия модулей",[2440,6901,6902],{"align":2426},"Высокий риск сломать старый функционал при выкате обновлений.",[2421,6904,6905,6910,6913,6916],{},[2440,6906,6907],{"align":2426},[15,6908,6909],{},"Сложность кода",[2440,6911,6912],{"align":2426},"SonarQube, Plato",[2440,6914,6915],{"align":2426},"Низкий индекс цикломатической сложности",[2440,6917,6918],{"align":2426},"Трудности чтения функций из-за глубокой вложенности ветвлений.",[26,6920,6922],{"id":6921},"_2-ключевые-точки-ручного-аудита","2. Ключевые точки ручного аудита",[862,6924,6926],{"id":6925},"а-работа-с-базой-данных-решение-проблемы-n1","А. Работа с базой данных: Решение проблемы N+1",[11,6928,6929],{},"Самая частая причина замедления работы API при росте базы данных — паттерн запросов N+1. Это происходит, когда бэкенд запрашивает список сущностей одним запросом, а затем в цикле делает отдельные SQL-запросы для получения связанных данных по каждой сущности.",[6931,6932,6934],"h4",{"id":6933},"неоптимальный-код-проблема-n1-на-nodejs","Неоптимальный код (Проблема N+1 на Node.js)",[34,6936,6938],{"className":73,"code":6937,"language":75,"meta":40,"style":40},"\u002F\u002F GET \u002Fapi\u002Fprojects - Выбирает N проектов, а затем делает N запросов к БД для получения авторов\napp.get('\u002Fapi\u002Fprojects', async (req, res) => {\n  const projects = await db.query('SELECT * FROM projects'); \u002F\u002F 1 запрос\n  const enrichedProjects = [];\n\n  for (const project of projects) {\n    \u002F\u002F ВЫПОЛНЯЕТСЯ N РАЗ! При 1000 проектов в БД мы совершим 1000 запросов.\n    const user = await db.query('SELECT * FROM users WHERE id = $1', [project.userId]); \n    enrichedProjects.push({ ...project, user });\n  }\n\n  res.json(enrichedProjects);\n});\n",[42,6939,6940,6945,6974,6999,7011,7015,7033,7038,7060,7077,7081,7085,7095],{"__ignoreMap":40},[79,6941,6942],{"class":81,"line":82},[79,6943,6944],{"class":393},"\u002F\u002F GET \u002Fapi\u002Fprojects - Выбирает N проектов, а затем делает N запросов к БД для получения авторов\n",[79,6946,6947,6949,6951,6953,6956,6958,6960,6962,6964,6966,6968,6970,6972],{"class":81,"line":103},[79,6948,4951],{"class":89},[79,6950,332],{"class":116},[79,6952,269],{"class":89},[79,6954,6955],{"class":96},"'\u002Fapi\u002Fprojects'",[79,6957,279],{"class":89},[79,6959,5540],{"class":85},[79,6961,355],{"class":89},[79,6963,4991],{"class":126},[79,6965,279],{"class":89},[79,6967,5005],{"class":126},[79,6969,5017],{"class":89},[79,6971,1023],{"class":85},[79,6973,120],{"class":89},[79,6975,6976,6978,6981,6983,6985,6987,6989,6991,6993,6996],{"class":81,"line":110},[79,6977,301],{"class":85},[79,6979,6980],{"class":133}," projects",[79,6982,307],{"class":85},[79,6984,4180],{"class":85},[79,6986,5701],{"class":89},[79,6988,5704],{"class":116},[79,6990,269],{"class":89},[79,6992,6604],{"class":96},[79,6994,6995],{"class":89},"); ",[79,6997,6998],{"class":393},"\u002F\u002F 1 запрос\n",[79,7000,7001,7003,7006,7008],{"class":81,"line":123},[79,7002,301],{"class":85},[79,7004,7005],{"class":133}," enrichedProjects",[79,7007,307],{"class":85},[79,7009,7010],{"class":89}," [];\n",[79,7012,7013],{"class":81,"line":139},[79,7014,107],{"emptyLinePlaceholder":106},[79,7016,7017,7020,7022,7024,7027,7030],{"class":81,"line":152},[79,7018,7019],{"class":85},"  for",[79,7021,355],{"class":89},[79,7023,4040],{"class":85},[79,7025,7026],{"class":133}," project",[79,7028,7029],{"class":85}," of",[79,7031,7032],{"class":89}," projects) {\n",[79,7034,7035],{"class":81,"line":165},[79,7036,7037],{"class":393},"    \u002F\u002F ВЫПОЛНЯЕТСЯ N РАЗ! При 1000 проектов в БД мы совершим 1000 запросов.\n",[79,7039,7040,7042,7044,7046,7048,7050,7052,7054,7057],{"class":81,"line":177},[79,7041,753],{"class":85},[79,7043,777],{"class":133},[79,7045,307],{"class":85},[79,7047,4180],{"class":85},[79,7049,5701],{"class":89},[79,7051,5704],{"class":116},[79,7053,269],{"class":89},[79,7055,7056],{"class":96},"'SELECT * FROM users WHERE id = $1'",[79,7058,7059],{"class":89},", [project.userId]); \n",[79,7061,7062,7065,7068,7071,7074],{"class":81,"line":189},[79,7063,7064],{"class":89},"    enrichedProjects.",[79,7066,7067],{"class":116},"push",[79,7069,7070],{"class":89},"({ ",[79,7072,7073],{"class":85},"...",[79,7075,7076],{"class":89},"project, user });\n",[79,7078,7079],{"class":81,"line":202},[79,7080,382],{"class":89},[79,7082,7083],{"class":81,"line":208},[79,7084,107],{"emptyLinePlaceholder":106},[79,7086,7087,7090,7092],{"class":81,"line":213},[79,7088,7089],{"class":89},"  res.",[79,7091,2583],{"class":116},[79,7093,7094],{"class":89},"(enrichedProjects);\n",[79,7096,7097],{"class":81,"line":223},[79,7098,3471],{"class":89},[6931,7100,7102],{"id":7101},"оптимальный-код-использование-sql-joins","Оптимальный код (Использование SQL JOINS)",[11,7104,7105],{},"Объединяем выборку в один запрос средствами СУБД:",[34,7107,7109],{"className":73,"code":7108,"language":75,"meta":40,"style":40},"app.get('\u002Fapi\u002Fprojects', async (req, res) => {\n  const query = `\n    SELECT p.*, row_to_json(u.*) as user \n    FROM projects p\n    LEFT JOIN users u ON p.userId = u.id\n  `;\n  const result = await db.query(query); \u002F\u002F 1 единственный запрос вместо N+1\n  res.json(result.rows);\n});\n",[42,7110,7111,7139,7151,7156,7161,7166,7173,7193,7202],{"__ignoreMap":40},[79,7112,7113,7115,7117,7119,7121,7123,7125,7127,7129,7131,7133,7135,7137],{"class":81,"line":82},[79,7114,4951],{"class":89},[79,7116,332],{"class":116},[79,7118,269],{"class":89},[79,7120,6955],{"class":96},[79,7122,279],{"class":89},[79,7124,5540],{"class":85},[79,7126,355],{"class":89},[79,7128,4991],{"class":126},[79,7130,279],{"class":89},[79,7132,5005],{"class":126},[79,7134,5017],{"class":89},[79,7136,1023],{"class":85},[79,7138,120],{"class":89},[79,7140,7141,7143,7146,7148],{"class":81,"line":103},[79,7142,301],{"class":85},[79,7144,7145],{"class":133}," query",[79,7147,307],{"class":85},[79,7149,7150],{"class":96}," `\n",[79,7152,7153],{"class":81,"line":110},[79,7154,7155],{"class":96},"    SELECT p.*, row_to_json(u.*) as user \n",[79,7157,7158],{"class":81,"line":123},[79,7159,7160],{"class":96},"    FROM projects p\n",[79,7162,7163],{"class":81,"line":139},[79,7164,7165],{"class":96},"    LEFT JOIN users u ON p.userId = u.id\n",[79,7167,7168,7171],{"class":81,"line":152},[79,7169,7170],{"class":96},"  `",[79,7172,100],{"class":89},[79,7174,7175,7177,7179,7181,7183,7185,7187,7190],{"class":81,"line":165},[79,7176,301],{"class":85},[79,7178,6591],{"class":133},[79,7180,307],{"class":85},[79,7182,4180],{"class":85},[79,7184,5701],{"class":89},[79,7186,5704],{"class":116},[79,7188,7189],{"class":89},"(query); ",[79,7191,7192],{"class":393},"\u002F\u002F 1 единственный запрос вместо N+1\n",[79,7194,7195,7197,7199],{"class":81,"line":177},[79,7196,7089],{"class":89},[79,7198,2583],{"class":116},[79,7200,7201],{"class":89},"(result.rows);\n",[79,7203,7204],{"class":81,"line":189},[79,7205,3471],{"class":89},[862,7207,7209],{"id":7208},"б-безопасность-хранения-секретов-и-валидация","Б. Безопасность хранения секретов и валидация",[1292,7211,7212,7225],{},[1232,7213,7214,7217,7218,7221,7222,60],{},[15,7215,7216],{},"Отсутствие учетных данных в Git",": Убедитесь, что в репозиторий не попали файлы ",[42,7219,7220],{},".env"," или строки с паролями. Для проверки истории коммитов используйте утилиту ",[42,7223,7224],{},"git-secrets",[1232,7226,7227,7230,7231,7234],{},[15,7228,7229],{},"Валидация переменных окружения на старте",": Используйте библиотеку ",[42,7232,7233],{},"zod"," для проверки наличия всех конфигурационных ключей при запуске приложения, чтобы упасть сразу, а не во время работы:",[34,7236,7238],{"className":73,"code":7237,"language":75,"meta":40,"style":40},"import { z } from 'zod';\n\nconst envSchema = z.object({\n  DATABASE_URL: z.string().url(),\n  PORT: z.string().transform(Number),\n  STRIPE_SECRET_KEY: z.string().min(10),\n});\n\nexport const ENV = envSchema.parse(process.env);\n",[42,7239,7240,7254,7258,7275,7291,7306,7326,7330,7334],{"__ignoreMap":40},[79,7241,7242,7244,7247,7249,7252],{"class":81,"line":82},[79,7243,86],{"class":85},[79,7245,7246],{"class":89}," { z } ",[79,7248,93],{"class":85},[79,7250,7251],{"class":96}," 'zod'",[79,7253,100],{"class":89},[79,7255,7256],{"class":81,"line":103},[79,7257,107],{"emptyLinePlaceholder":106},[79,7259,7260,7262,7265,7267,7270,7273],{"class":81,"line":110},[79,7261,4040],{"class":85},[79,7263,7264],{"class":133}," envSchema",[79,7266,307],{"class":85},[79,7268,7269],{"class":89}," z.",[79,7271,7272],{"class":116},"object",[79,7274,888],{"class":89},[79,7276,7277,7280,7282,7285,7288],{"class":81,"line":123},[79,7278,7279],{"class":89},"  DATABASE_URL: z.",[79,7281,4165],{"class":116},[79,7283,7284],{"class":89},"().",[79,7286,7287],{"class":116},"url",[79,7289,7290],{"class":89},"(),\n",[79,7292,7293,7296,7298,7300,7303],{"class":81,"line":139},[79,7294,7295],{"class":89},"  PORT: z.",[79,7297,4165],{"class":116},[79,7299,7284],{"class":89},[79,7301,7302],{"class":116},"transform",[79,7304,7305],{"class":89},"(Number),\n",[79,7307,7308,7311,7313,7315,7318,7320,7323],{"class":81,"line":152},[79,7309,7310],{"class":89},"  STRIPE_SECRET_KEY: z.",[79,7312,4165],{"class":116},[79,7314,7284],{"class":89},[79,7316,7317],{"class":116},"min",[79,7319,269],{"class":89},[79,7321,7322],{"class":133},"10",[79,7324,7325],{"class":89},"),\n",[79,7327,7328],{"class":81,"line":165},[79,7329,3471],{"class":89},[79,7331,7332],{"class":81,"line":177},[79,7333,107],{"emptyLinePlaceholder":106},[79,7335,7336,7338,7341,7344,7346,7349,7351],{"class":81,"line":189},[79,7337,260],{"class":85},[79,7339,7340],{"class":85}," const",[79,7342,7343],{"class":133}," ENV",[79,7345,307],{"class":85},[79,7347,7348],{"class":89}," envSchema.",[79,7350,797],{"class":116},[79,7352,7353],{"class":89},"(process.env);\n",[862,7355,7357],{"id":7356},"в-фильтрация-ввода-и-заголовки-безопасности","В. Фильтрация ввода и заголовки безопасности",[1292,7359,7360,7374],{},[1232,7361,7362,7365,7366,7369,7370,7373],{},[15,7363,7364],{},"Защита от SQL-инъекций",": Проверьте, чтобы в коде не было прямой конкатенации строк при составлении запросов (например, ",[42,7367,7368],{},"db.query(\"SELECT * FROM users WHERE id = \" + id)","). Всегда используйте параметризованные запросы (",[42,7371,7372],{},"db.query(\"... WHERE id = $1\", [id])",") или ORM.",[1232,7375,7376,7379,7380,7383],{},[15,7377,7378],{},"Защита от XSS и Clickjacking",": Убедитесь, что в приложении подключен пакет ",[42,7381,7382],{},"helmet",", который автоматически настраивает безопасные HTTP-заголовки (Content-Security-Policy, HSTS, X-Frame-Options).",[26,7385,7387],{"id":7386},"_3-разработка-плана-устранения-технического-долга","3. Разработка плана устранения технического долга",[11,7389,7390],{},"После фиксации всех проблем разделите задачи рефакторинга по приоритетам:",[1229,7392,7393,7399,7405],{},[1232,7394,7395,7398],{},[15,7396,7397],{},"Критические проблемы (P0)",": Дыры в безопасности, пароли в репозитории, отсутствие резервных копий БД, ошибки авторизации. Исправлять немедленно.",[1232,7400,7401,7404],{},[15,7402,7403],{},"Проблемы производительности (P1)",": Отсутствие индексов на внешних ключах, запросы N+1 в ключевых сценариях, отсутствие кэширования для тяжелых запросов. Запланировать рефакторинг на ближайший спринт.",[1232,7406,7407,7410],{},[15,7408,7409],{},"Чистка кодовой базы (P2)",": Дублирование кода, несоблюдение форматирования, покрытие тестами служебных модулей. Выделять на эти задачи 20% времени каждого планового спринта.",[26,7412,1290],{"id":1289},[1292,7414,7415,7421,7428],{},[1232,7416,7417,7420],{},[1270,7418,4709],{"href":2561,"rel":7419},[1300]," — эталонный список веб-уязвимостей для блока безопасности",[1232,7422,7423,7427],{},[1270,7424,6863],{"href":7425,"rel":7426},"https:\u002F\u002Fdocs.npmjs.com\u002Fcli\u002Fcommands\u002Fnpm-audit",[1300]," — проверка зависимостей на известные уязвимости",[1232,7429,7430,7435],{},[1270,7431,7434],{"href":7432,"rel":7433},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Findexes.html",[1300],"PostgreSQL: Indexes"," — официальное руководство по индексированию",[11,7437,7438,7439,60],{},"Своевременный технический аудит позволяет сохранять высокую скорость разработки новых функций и гарантирует стабильную работу проекта под нагрузкой. Проведение таких проверок особенно критично, если вы подозреваете, что код вашего проекта создавался наспех бюджетными подрядчиками. Подробнее об этом читайте в моем анализе ",[1270,7440,4445],{"href":3764},[11,7442,7443,7444,7447],{},"Если вашему проекту необходим независимый экспертный технический аудит кодовой базы, оценка архитектуры перед масштабированием или разработка стандартов качества кода для вашей команды, ознакомьтесь с услугой ",[1270,7445,7446],{"href":1324},"Технической консультации"," или запишитесь на сессию.",[1328,7449,7450],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}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 .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":7452},[7453,7454,7459,7460],{"id":6827,"depth":103,"text":6828},{"id":6921,"depth":103,"text":6922,"children":7455},[7456,7457,7458],{"id":6925,"depth":110,"text":6926},{"id":7208,"depth":110,"text":7209},{"id":7356,"depth":110,"text":7357},{"id":7386,"depth":103,"text":7387},{"id":1289,"depth":103,"text":1290},"2026-06-07","Практическое техническое руководство по проведению аудита легаси-проектов на Node.js\u002FTypeScript, поиску уязвимостей, анализу индексов БД и решению проблемы N+1.",[7464,7467,7470],{"q":7465,"a":7466},"Когда нужен технический аудит кода?","В трёх критических точках: при приёме легаси-проекта от другой команды, при покупке готового продукта и перед этапом активного масштабирования. Аудит на 2–3 неделе разработки нового проекта дешевле любого аудита после релиза.",{"q":7468,"a":7469},"Что проверяется в первую очередь?","Автоматические метрики (уязвимости зависимостей через npm audit, линтинг, покрытие тестами), затем ручные точки: параметризация SQL-запросов, индексы на внешних ключах, проблема N+1, секреты в репозитории и обработка ошибок.",{"q":7471,"a":7472},"Что делать с найденными проблемами?","Разделить по приоритетам: P0 — безопасность и бэкапы, исправлять немедленно; P1 — производительность (индексы, N+1, кэш), в ближайший спринт; P2 — чистка кода, стабильные 20% времени каждого спринта.","\u002Fimages\u002Fblog\u002Fblog_code_audit.jpg",{},"\u002Fblog\u002Fru\u002Fcode-audit-technical-debt",{"title":6807,"description":7462},"blog\u002Fru\u002Fcode-audit-technical-debt",[4440,7479,7480,4747,7481],"КодРевью","Безопасность","Техдолг","Ime586W2QwEatWxzjto2rtqIB-50VdS2FF-JVgIo83Y",{"id":7484,"title":7485,"body":7486,"date":8886,"description":8887,"extension":1344,"faq":8888,"image":8898,"lang":1356,"meta":8899,"navigation":106,"path":8900,"published":106,"readTime":213,"seo":8901,"stem":8902,"tags":8903,"updated":1342,"__hash__":8906},"blog\u002Fblog\u002Fru\u002Ftelegram-bot-ai-rag-support.md","ИИ-поддержка клиентов: Как собрать RAG-бота с бесшовной передачей менеджеру",{"type":8,"value":7487,"toc":8878},[7488,7491,7498,7505,7512,7516,7522,7526,7536,7606,7609,7693,7697,7700,8111,8115,8118,8370,8374,8381,8820,8826,8837,8839,8864,8867,8875],[11,7489,7490],{},"Внедрение больших языковых моделей (LLM) вроде ChatGPT для ответов на вопросы клиентов — один из самых популярных способов снижения нагрузки на техподдержку. Однако стандартные ИИ-модели имеют два критических недостатка для корпоративного использования: у них нет доступа к вашим закрытым данным (инструкции, цены, API) и они склонны придумывать недостоверные факты («галлюцинировать»).",[11,7492,7493,7494,7497],{},"Для решения этих проблем в продакшене используется архитектура ",[15,7495,7496],{},"RAG (Retrieval-Augmented Generation)",". Сначала бот ищет ответ в локальной базе знаний компании, извлекает релевантные текстовые фрагменты и передает их в качестве контекста в LLM. Модель формулирует ответ строго на основе предоставленных документов.",[11,7499,7500,7501,7504],{},"Для обеспечения премиального клиентского сервиса система должна содержать ",[15,7502,7503],{},"протокол эскалации на оператора",". Если ИИ не находит ответа в базе, диалог мгновенно переключается на человека.",[11,7506,7507,7508,7511],{},"В этой статье мы соберем полноценный RAG-цикл на Node.js, настроим поиск в PostgreSQL с расширением ",[42,7509,7510],{},"pgvector"," и опишем логику переключения диалога.",[26,7513,7515],{"id":7514},"архитектура-работы-rag-бота-поддержки","Архитектура работы RAG-бота поддержки",[34,7517,7520],{"className":7518,"code":7519,"language":39,"meta":40},[37],"[Пользователь спрашивает: \"Как настроить Stripe?\"]\n                       │\n                       v\n     (Генерация векторного эмбеддинга)\n     [text-embedding-3-small] (1536 dim)\n                       │\n                       v\n     (Векторный поиск в PostgreSQL)\n     SELECT * FROM match_kb_docs(vector, threshold = 0.75)\n                       │\n                       +-----------------------+\n                       |                       |\n            (Контекст НАЙДЕН)         (Контекст НЕ НАЙДЕН \u002F Низкий скор)\n                       │                       │\n                       v                       v\n     [Формируем промпт с текстом KB]   [Ставим сессии статус human_handling]\n                       │                       │\n                       v                       v\n     [Отправляем запрос в gpt-4o-mini]  [Оповещаем менеджеров в Slack\u002FCRM]\n                       │                       │\n                       v                       v\n     [Отправляем ИИ-ответ клиенту]     [Отправляем: \"Перевожу на менеджера\"]\n",[42,7521,7519],{"__ignoreMap":40},[26,7523,7525],{"id":7524},"_1-настройка-базы-данных-pgvector","1. Настройка базы данных Pgvector",[11,7527,7528,7529,7532,7533,441],{},"Включите расширение ",[42,7530,7531],{},"vector"," в вашей БД PostgreSQL и создайте таблицу для хранения фрагментов документации. Количество измерений (dimensions) вектора должно совпадать с выбранной моделью эмбеддингов (1536 для OpenAI ",[42,7534,7535],{},"text-embedding-3-small",[34,7537,7539],{"className":6289,"code":7538,"language":6291,"meta":40,"style":40},"-- Включаем поддержку векторного расширения\nCREATE EXTENSION IF NOT EXISTS vector;\n\n-- Создаем таблицу Базы Знаний\nCREATE TABLE kb_documents (\n  id SERIAL PRIMARY KEY,\n  content TEXT NOT NULL,\n  embedding VECTOR(1536) NOT NULL,\n  category VARCHAR(50) DEFAULT 'general',\n  created_at TIMESTAMP DEFAULT NOW()\n);\n\n-- Индексируем векторную колонку (используем HNSW для высокой скорости на масштабе)\nCREATE INDEX kb_hnsw_idx ON kb_documents USING hnsw (embedding vector_cosine_ops);\n",[42,7540,7541,7546,7551,7555,7560,7565,7569,7574,7579,7584,7588,7592,7596,7601],{"__ignoreMap":40},[79,7542,7543],{"class":81,"line":82},[79,7544,7545],{},"-- Включаем поддержку векторного расширения\n",[79,7547,7548],{"class":81,"line":103},[79,7549,7550],{},"CREATE EXTENSION IF NOT EXISTS vector;\n",[79,7552,7553],{"class":81,"line":110},[79,7554,107],{"emptyLinePlaceholder":106},[79,7556,7557],{"class":81,"line":123},[79,7558,7559],{},"-- Создаем таблицу Базы Знаний\n",[79,7561,7562],{"class":81,"line":139},[79,7563,7564],{},"CREATE TABLE kb_documents (\n",[79,7566,7567],{"class":81,"line":152},[79,7568,6308],{},[79,7570,7571],{"class":81,"line":165},[79,7572,7573],{},"  content TEXT NOT NULL,\n",[79,7575,7576],{"class":81,"line":177},[79,7577,7578],{},"  embedding VECTOR(1536) NOT NULL,\n",[79,7580,7581],{"class":81,"line":189},[79,7582,7583],{},"  category VARCHAR(50) DEFAULT 'general',\n",[79,7585,7586],{"class":81,"line":202},[79,7587,6318],{},[79,7589,7590],{"class":81,"line":208},[79,7591,340],{},[79,7593,7594],{"class":81,"line":213},[79,7595,107],{"emptyLinePlaceholder":106},[79,7597,7598],{"class":81,"line":223},[79,7599,7600],{},"-- Индексируем векторную колонку (используем HNSW для высокой скорости на масштабе)\n",[79,7602,7603],{"class":81,"line":235},[79,7604,7605],{},"CREATE INDEX kb_hnsw_idx ON kb_documents USING hnsw (embedding vector_cosine_ops);\n",[11,7607,7608],{},"Создадим SQL-функцию поиска документов по косинусному расстоянию:",[34,7610,7612],{"className":6289,"code":7611,"language":6291,"meta":40,"style":40},"CREATE OR REPLACE FUNCTION match_kb_docs(\n  query_embedding VECTOR(1536),\n  match_threshold FLOAT,\n  match_count INT\n)\nRETURNS TABLE (id INT, content TEXT, similarity FLOAT)\nLANGUAGE plpgsql AS $$\nBEGIN\n  RETURN QUERY\n  SELECT kb.id, kb.content, 1 - (kb.embedding \u003C=> query_embedding) AS similarity\n  FROM kb_documents kb\n  WHERE 1 - (kb.embedding \u003C=> query_embedding) > match_threshold\n  ORDER BY kb.embedding \u003C=> query_embedding\n  LIMIT match_count;\nEND;\n$$;\n",[42,7613,7614,7619,7624,7629,7634,7638,7643,7648,7653,7658,7663,7668,7673,7678,7683,7688],{"__ignoreMap":40},[79,7615,7616],{"class":81,"line":82},[79,7617,7618],{},"CREATE OR REPLACE FUNCTION match_kb_docs(\n",[79,7620,7621],{"class":81,"line":103},[79,7622,7623],{},"  query_embedding VECTOR(1536),\n",[79,7625,7626],{"class":81,"line":110},[79,7627,7628],{},"  match_threshold FLOAT,\n",[79,7630,7631],{"class":81,"line":123},[79,7632,7633],{},"  match_count INT\n",[79,7635,7636],{"class":81,"line":139},[79,7637,513],{},[79,7639,7640],{"class":81,"line":152},[79,7641,7642],{},"RETURNS TABLE (id INT, content TEXT, similarity FLOAT)\n",[79,7644,7645],{"class":81,"line":165},[79,7646,7647],{},"LANGUAGE plpgsql AS $$\n",[79,7649,7650],{"class":81,"line":177},[79,7651,7652],{},"BEGIN\n",[79,7654,7655],{"class":81,"line":189},[79,7656,7657],{},"  RETURN QUERY\n",[79,7659,7660],{"class":81,"line":202},[79,7661,7662],{},"  SELECT kb.id, kb.content, 1 - (kb.embedding \u003C=> query_embedding) AS similarity\n",[79,7664,7665],{"class":81,"line":208},[79,7666,7667],{},"  FROM kb_documents kb\n",[79,7669,7670],{"class":81,"line":213},[79,7671,7672],{},"  WHERE 1 - (kb.embedding \u003C=> query_embedding) > match_threshold\n",[79,7674,7675],{"class":81,"line":223},[79,7676,7677],{},"  ORDER BY kb.embedding \u003C=> query_embedding\n",[79,7679,7680],{"class":81,"line":235},[79,7681,7682],{},"  LIMIT match_count;\n",[79,7684,7685],{"class":81,"line":247},[79,7686,7687],{},"END;\n",[79,7689,7690],{"class":81,"line":252},[79,7691,7692],{},"$$;\n",[26,7694,7696],{"id":7695},"_2-реализация-векторного-поиска-на-nodejs","2. Реализация векторного поиска на Node.js",[11,7698,7699],{},"Напишем сервис, который будет обращаться к API OpenAI для генерации вектора из вопроса пользователя и осуществлять поиск по базе PostgreSQL:",[34,7701,7703],{"className":73,"code":7702,"language":75,"meta":40,"style":40},"import { OpenAI } from 'openai';\nimport pg from 'pg';\n\nconst openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\nconst pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });\n\nexport async function getEmbedding(text: string): Promise\u003Cnumber[]> {\n  const response = await openai.embeddings.create({\n    model: 'text-embedding-3-small',\n    input: text.replace(\u002F\\n\u002Fg, ' '),\n  });\n  return response.data[0].embedding;\n}\n\nexport async function findRelevantContext(queryText: string): Promise\u003Cstring> {\n  const queryVector = await getEmbedding(queryText);\n  const sql = `SELECT * FROM match_kb_docs($1::vector, 0.75, 3)`;\n  \n  const client = await pool.connect();\n  try {\n    const result = await client.query(sql, [JSON.stringify(queryVector)]);\n    if (result.rows.length === 0) {\n      return '';\n    }\n    \u002F\u002F Объединяем найденные статьи в один текстовый блок\n    return result.rows.map(row => row.content).join('\\n---\\n');\n  } finally {\n    client.release();\n  }\n}\n",[42,7704,7705,7719,7732,7736,7758,7781,7785,7817,7835,7845,7872,7876,7888,7892,7896,7928,7944,7958,7962,7979,7985,8013,8031,8040,8044,8049,8085,8093,8103,8107],{"__ignoreMap":40},[79,7706,7707,7709,7712,7714,7717],{"class":81,"line":82},[79,7708,86],{"class":85},[79,7710,7711],{"class":89}," { OpenAI } ",[79,7713,93],{"class":85},[79,7715,7716],{"class":96}," 'openai'",[79,7718,100],{"class":89},[79,7720,7721,7723,7726,7728,7730],{"class":81,"line":103},[79,7722,86],{"class":85},[79,7724,7725],{"class":89}," pg ",[79,7727,93],{"class":85},[79,7729,6449],{"class":96},[79,7731,100],{"class":89},[79,7733,7734],{"class":81,"line":110},[79,7735,107],{"emptyLinePlaceholder":106},[79,7737,7738,7740,7743,7745,7747,7750,7753,7756],{"class":81,"line":123},[79,7739,4040],{"class":85},[79,7741,7742],{"class":133}," openai",[79,7744,307],{"class":85},[79,7746,310],{"class":85},[79,7748,7749],{"class":116}," OpenAI",[79,7751,7752],{"class":89},"({ apiKey: process.env.",[79,7754,7755],{"class":133},"OPENAI_API_KEY",[79,7757,5458],{"class":89},[79,7759,7760,7762,7765,7767,7769,7772,7775,7777,7779],{"class":81,"line":139},[79,7761,4040],{"class":85},[79,7763,7764],{"class":133}," pool",[79,7766,307],{"class":85},[79,7768,310],{"class":85},[79,7770,7771],{"class":89}," pg.",[79,7773,7774],{"class":116},"Pool",[79,7776,6502],{"class":89},[79,7778,6505],{"class":133},[79,7780,5458],{"class":89},[79,7782,7783],{"class":81,"line":152},[79,7784,107],{"emptyLinePlaceholder":106},[79,7786,7787,7789,7791,7793,7796,7798,7800,7802,7804,7806,7808,7810,7812,7815],{"class":81,"line":165},[79,7788,260],{"class":85},[79,7790,4139],{"class":85},[79,7792,263],{"class":85},[79,7794,7795],{"class":116}," getEmbedding",[79,7797,269],{"class":89},[79,7799,39],{"class":126},[79,7801,130],{"class":85},[79,7803,147],{"class":133},[79,7805,289],{"class":89},[79,7807,130],{"class":85},[79,7809,4160],{"class":116},[79,7811,997],{"class":89},[79,7813,7814],{"class":133},"number",[79,7816,6485],{"class":89},[79,7818,7819,7821,7824,7826,7828,7831,7833],{"class":81,"line":177},[79,7820,301],{"class":85},[79,7822,7823],{"class":133}," response",[79,7825,307],{"class":85},[79,7827,4180],{"class":85},[79,7829,7830],{"class":89}," openai.embeddings.",[79,7832,4186],{"class":116},[79,7834,888],{"class":89},[79,7836,7837,7840,7843],{"class":81,"line":189},[79,7838,7839],{"class":89},"    model: ",[79,7841,7842],{"class":96},"'text-embedding-3-small'",[79,7844,2604],{"class":89},[79,7846,7847,7850,7853,7855,7858,7860,7862,7865,7867,7870],{"class":81,"line":202},[79,7848,7849],{"class":89},"    input: text.",[79,7851,7852],{"class":116},"replace",[79,7854,269],{"class":89},[79,7856,7857],{"class":96},"\u002F",[79,7859,529],{"class":133},[79,7861,7857],{"class":96},[79,7863,7864],{"class":85},"g",[79,7866,279],{"class":89},[79,7868,7869],{"class":96},"' '",[79,7871,7325],{"class":89},[79,7873,7874],{"class":81,"line":208},[79,7875,1195],{"class":89},[79,7877,7878,7880,7883,7885],{"class":81,"line":213},[79,7879,1204],{"class":85},[79,7881,7882],{"class":89}," response.data[",[79,7884,5652],{"class":133},[79,7886,7887],{"class":89},"].embedding;\n",[79,7889,7890],{"class":81,"line":223},[79,7891,205],{"class":89},[79,7893,7894],{"class":81,"line":235},[79,7895,107],{"emptyLinePlaceholder":106},[79,7897,7898,7900,7902,7904,7907,7909,7912,7914,7916,7918,7920,7922,7924,7926],{"class":81,"line":247},[79,7899,260],{"class":85},[79,7901,4139],{"class":85},[79,7903,263],{"class":85},[79,7905,7906],{"class":116}," findRelevantContext",[79,7908,269],{"class":89},[79,7910,7911],{"class":126},"queryText",[79,7913,130],{"class":85},[79,7915,147],{"class":133},[79,7917,289],{"class":89},[79,7919,130],{"class":85},[79,7921,4160],{"class":116},[79,7923,997],{"class":89},[79,7925,4165],{"class":133},[79,7927,4168],{"class":89},[79,7929,7930,7932,7935,7937,7939,7941],{"class":81,"line":252},[79,7931,301],{"class":85},[79,7933,7934],{"class":133}," queryVector",[79,7936,307],{"class":85},[79,7938,4180],{"class":85},[79,7940,7795],{"class":116},[79,7942,7943],{"class":89},"(queryText);\n",[79,7945,7946,7948,7951,7953,7956],{"class":81,"line":257},[79,7947,301],{"class":85},[79,7949,7950],{"class":133}," sql",[79,7952,307],{"class":85},[79,7954,7955],{"class":96}," `SELECT * FROM match_kb_docs($1::vector, 0.75, 3)`",[79,7957,100],{"class":89},[79,7959,7960],{"class":81,"line":298},[79,7961,346],{"class":89},[79,7963,7964,7966,7968,7970,7972,7975,7977],{"class":81,"line":319},[79,7965,301],{"class":85},[79,7967,6492],{"class":133},[79,7969,307],{"class":85},[79,7971,4180],{"class":85},[79,7973,7974],{"class":89}," pool.",[79,7976,6517],{"class":116},[79,7978,447],{"class":89},[79,7980,7981,7983],{"class":81,"line":343},[79,7982,745],{"class":85},[79,7984,120],{"class":89},[79,7986,7987,7989,7991,7993,7995,7997,7999,8002,8005,8007,8010],{"class":81,"line":349},[79,7988,753],{"class":85},[79,7990,6591],{"class":133},[79,7992,307],{"class":85},[79,7994,4180],{"class":85},[79,7996,6514],{"class":89},[79,7998,5704],{"class":116},[79,8000,8001],{"class":89},"(sql, [",[79,8003,8004],{"class":133},"JSON",[79,8006,60],{"class":89},[79,8008,8009],{"class":116},"stringify",[79,8011,8012],{"class":89},"(queryVector)]);\n",[79,8014,8015,8017,8020,8023,8026,8029],{"class":81,"line":364},[79,8016,1051],{"class":85},[79,8018,8019],{"class":89}," (result.rows.",[79,8021,8022],{"class":133},"length",[79,8024,8025],{"class":85}," ===",[79,8027,8028],{"class":133}," 0",[79,8030,5161],{"class":89},[79,8032,8033,8035,8038],{"class":81,"line":379},[79,8034,5060],{"class":85},[79,8036,8037],{"class":96}," ''",[79,8039,100],{"class":89},[79,8041,8042],{"class":81,"line":385},[79,8043,1190],{"class":89},[79,8045,8046],{"class":81,"line":390},[79,8047,8048],{"class":393},"    \u002F\u002F Объединяем найденные статьи в один текстовый блок\n",[79,8050,8051,8053,8056,8058,8060,8063,8065,8068,8070,8072,8074,8076,8079,8081,8083],{"class":81,"line":397},[79,8052,367],{"class":85},[79,8054,8055],{"class":89}," result.rows.",[79,8057,480],{"class":116},[79,8059,269],{"class":89},[79,8061,8062],{"class":126},"row",[79,8064,429],{"class":85},[79,8066,8067],{"class":89}," row.content).",[79,8069,521],{"class":116},[79,8071,269],{"class":89},[79,8073,526],{"class":96},[79,8075,529],{"class":133},[79,8077,8078],{"class":96},"---",[79,8080,529],{"class":133},[79,8082,526],{"class":96},[79,8084,340],{"class":89},[79,8086,8087,8089,8091],{"class":81,"line":450},[79,8088,826],{"class":89},[79,8090,6669],{"class":85},[79,8092,120],{"class":89},[79,8094,8095,8098,8101],{"class":81,"line":455},[79,8096,8097],{"class":89},"    client.",[79,8099,8100],{"class":116},"release",[79,8102,447],{"class":89},[79,8104,8105],{"class":81,"line":461},[79,8106,382],{"class":89},[79,8108,8109],{"class":81,"line":474},[79,8110,205],{"class":89},[26,8112,8114],{"id":8113},"_3-системный-промпт-против-галлюцинаций-ии","3. Системный промпт против галлюцинаций ИИ",[11,8116,8117],{},"Чтобы модель не придумывала несуществующие тарифы и правила, промпт должен жестко запретить генерацию ответов при отсутствии информации в контексте:",[34,8119,8121],{"className":73,"code":8120,"language":75,"meta":40,"style":40},"export async function generateAIResponse(userQuestion: string, context: string): Promise\u003Cstring> {\n  if (!context) {\n    return 'ERROR_UNRESOLVED';\n  }\n\n  const systemPrompt = `\nВы — оператор техподдержки TeleGo.io.\nОтвечайте на вопросы пользователя, используя ТОЛЬКО предоставленный контекст.\nПравила:\n1. Опирайтесь исключительно на предоставленные факты. Не придумывайте ничего от себя.\n2. Если в контексте нет ответа на вопрос или он не связан с темой, ответьте строго одним словом: \"ERROR_UNRESOLVED\".\n3. Пишите вежливо, в деловом стиле.\n\nКонтекст:\n---\n${context}\n---\n`;\n\n  const response = await openai.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [\n      { role: 'system', content: systemPrompt },\n      { role: 'user', content: userQuestion }\n    ],\n    temperature: 0.0, \u002F\u002F Нулевая температура гарантирует стабильный результат\n  });\n\n  return response.choices[0].message.content || 'ERROR_UNRESOLVED';\n}\n",[42,8122,8123,8164,8175,8184,8188,8192,8203,8208,8213,8218,8223,8228,8233,8237,8242,8247,8256,8260,8267,8271,8288,8297,8302,8313,8322,8326,8339,8343,8347,8366],{"__ignoreMap":40},[79,8124,8125,8127,8129,8131,8134,8136,8139,8141,8143,8145,8148,8150,8152,8154,8156,8158,8160,8162],{"class":81,"line":82},[79,8126,260],{"class":85},[79,8128,4139],{"class":85},[79,8130,263],{"class":85},[79,8132,8133],{"class":116}," generateAIResponse",[79,8135,269],{"class":89},[79,8137,8138],{"class":126},"userQuestion",[79,8140,130],{"class":85},[79,8142,147],{"class":133},[79,8144,279],{"class":89},[79,8146,8147],{"class":126},"context",[79,8149,130],{"class":85},[79,8151,147],{"class":133},[79,8153,289],{"class":89},[79,8155,130],{"class":85},[79,8157,4160],{"class":116},[79,8159,997],{"class":89},[79,8161,4165],{"class":133},[79,8163,4168],{"class":89},[79,8165,8166,8168,8170,8172],{"class":81,"line":103},[79,8167,352],{"class":85},[79,8169,355],{"class":89},[79,8171,358],{"class":85},[79,8173,8174],{"class":89},"context) {\n",[79,8176,8177,8179,8182],{"class":81,"line":110},[79,8178,367],{"class":85},[79,8180,8181],{"class":96}," 'ERROR_UNRESOLVED'",[79,8183,100],{"class":89},[79,8185,8186],{"class":81,"line":123},[79,8187,382],{"class":89},[79,8189,8190],{"class":81,"line":139},[79,8191,107],{"emptyLinePlaceholder":106},[79,8193,8194,8196,8199,8201],{"class":81,"line":152},[79,8195,301],{"class":85},[79,8197,8198],{"class":133}," systemPrompt",[79,8200,307],{"class":85},[79,8202,7150],{"class":96},[79,8204,8205],{"class":81,"line":165},[79,8206,8207],{"class":96},"Вы — оператор техподдержки TeleGo.io.\n",[79,8209,8210],{"class":81,"line":177},[79,8211,8212],{"class":96},"Отвечайте на вопросы пользователя, используя ТОЛЬКО предоставленный контекст.\n",[79,8214,8215],{"class":81,"line":189},[79,8216,8217],{"class":96},"Правила:\n",[79,8219,8220],{"class":81,"line":202},[79,8221,8222],{"class":96},"1. Опирайтесь исключительно на предоставленные факты. Не придумывайте ничего от себя.\n",[79,8224,8225],{"class":81,"line":208},[79,8226,8227],{"class":96},"2. Если в контексте нет ответа на вопрос или он не связан с темой, ответьте строго одним словом: \"ERROR_UNRESOLVED\".\n",[79,8229,8230],{"class":81,"line":213},[79,8231,8232],{"class":96},"3. Пишите вежливо, в деловом стиле.\n",[79,8234,8235],{"class":81,"line":223},[79,8236,107],{"emptyLinePlaceholder":106},[79,8238,8239],{"class":81,"line":235},[79,8240,8241],{"class":96},"Контекст:\n",[79,8243,8244],{"class":81,"line":247},[79,8245,8246],{"class":96},"---\n",[79,8248,8249,8252,8254],{"class":81,"line":252},[79,8250,8251],{"class":96},"${",[79,8253,8147],{"class":89},[79,8255,205],{"class":96},[79,8257,8258],{"class":81,"line":257},[79,8259,8246],{"class":96},[79,8261,8262,8265],{"class":81,"line":298},[79,8263,8264],{"class":96},"`",[79,8266,100],{"class":89},[79,8268,8269],{"class":81,"line":319},[79,8270,107],{"emptyLinePlaceholder":106},[79,8272,8273,8275,8277,8279,8281,8284,8286],{"class":81,"line":343},[79,8274,301],{"class":85},[79,8276,7823],{"class":133},[79,8278,307],{"class":85},[79,8280,4180],{"class":85},[79,8282,8283],{"class":89}," openai.chat.completions.",[79,8285,4186],{"class":116},[79,8287,888],{"class":89},[79,8289,8290,8292,8295],{"class":81,"line":349},[79,8291,7839],{"class":89},[79,8293,8294],{"class":96},"'gpt-4o-mini'",[79,8296,2604],{"class":89},[79,8298,8299],{"class":81,"line":364},[79,8300,8301],{"class":89},"    messages: [\n",[79,8303,8304,8307,8310],{"class":81,"line":379},[79,8305,8306],{"class":89},"      { role: ",[79,8308,8309],{"class":96},"'system'",[79,8311,8312],{"class":89},", content: systemPrompt },\n",[79,8314,8315,8317,8319],{"class":81,"line":385},[79,8316,8306],{"class":89},[79,8318,767],{"class":96},[79,8320,8321],{"class":89},", content: userQuestion }\n",[79,8323,8324],{"class":81,"line":390},[79,8325,4232],{"class":89},[79,8327,8328,8331,8334,8336],{"class":81,"line":397},[79,8329,8330],{"class":89},"    temperature: ",[79,8332,8333],{"class":133},"0.0",[79,8335,279],{"class":89},[79,8337,8338],{"class":393},"\u002F\u002F Нулевая температура гарантирует стабильный результат\n",[79,8340,8341],{"class":81,"line":450},[79,8342,1195],{"class":89},[79,8344,8345],{"class":81,"line":455},[79,8346,107],{"emptyLinePlaceholder":106},[79,8348,8349,8351,8354,8356,8359,8362,8364],{"class":81,"line":461},[79,8350,1204],{"class":85},[79,8352,8353],{"class":89}," response.choices[",[79,8355,5652],{"class":133},[79,8357,8358],{"class":89},"].message.content ",[79,8360,8361],{"class":85},"||",[79,8363,8181],{"class":96},[79,8365,100],{"class":89},[79,8367,8368],{"class":81,"line":474},[79,8369,205],{"class":89},[26,8371,8373],{"id":8372},"_4-логика-маршрутизации-и-передача-человеку","4. Логика маршрутизации и передача человеку",[11,8375,8376,8377,8380],{},"При получении сообщения в Telegram проверяем, не общается ли пользователь с живым оператором. Если нет — запускаем RAG-цикл. Если ИИ вернул ",[42,8378,8379],{},"ERROR_UNRESOLVED"," — переключаем статус чата.",[34,8382,8384],{"className":73,"code":8383,"language":75,"meta":40,"style":40},"import TelegramBot from 'node-telegram-bot-api';\n\nconst bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN!, { polling: true });\n\nbot.on('message', async (msg) => {\n  const chatId = msg.chat.id;\n  const userText = msg.text;\n\n  if (!userText) return;\n\n  \u002F\u002F 1. Получаем статус сессии из БД\n  const session = await db.getChatSession(chatId);\n\n  \u002F\u002F Если чат переключен на человека, пересылаем сообщения в Slack\u002Fпанель менеджеров\n  if (session?.status === 'human_handling') {\n    await forwardToManagers(chatId, msg);\n    return;\n  }\n\n  \u002F\u002F 2. Выполняем RAG-поиск\n  try {\n    const context = await findRelevantContext(userText);\n    const aiResponse = await generateAIResponse(userText, context);\n\n    \u002F\u002F 3. Проверка на эскалацию\n    if (aiResponse.trim() === 'ERROR_UNRESOLVED') {\n      \u002F\u002F Обновляем статус сессии в базе данных\n      await db.updateChatSession(chatId, { status: 'human_handling' });\n      \n      \u002F\u002F Оповещаем менеджеров через вебхук\n      await notifySupportStaff(chatId, userText);\n      \n      await bot.sendMessage(\n        chatId, \n        \"К сожалению, я не нашел ответ в документации. Я переключил этот чат на службу поддержки. Специалист ответит вам в ближайшее время.\"\n      );\n      return;\n    }\n\n    \u002F\u002F 4. Отправляем ИИ-ответ пользователю\n    await bot.sendMessage(chatId, aiResponse);\n\n  } catch (error) {\n    console.error('Ошибка в цикле RAG:', error);\n    await bot.sendMessage(chatId, \"Произошла ошибка. Перевожу диалог на оператора.\");\n    await db.updateChatSession(chatId, { status: 'human_handling' });\n  }\n});\n",[42,8385,8386,8400,8404,8432,8436,8464,8476,8488,8492,8507,8511,8516,8534,8538,8543,8557,8567,8573,8577,8581,8586,8592,8608,8624,8628,8633,8652,8657,8675,8679,8684,8694,8698,8710,8715,8720,8725,8731,8735,8739,8744,8755,8759,8767,8782,8798,8812,8816],{"__ignoreMap":40},[79,8387,8388,8390,8393,8395,8398],{"class":81,"line":82},[79,8389,86],{"class":85},[79,8391,8392],{"class":89}," TelegramBot ",[79,8394,93],{"class":85},[79,8396,8397],{"class":96}," 'node-telegram-bot-api'",[79,8399,100],{"class":89},[79,8401,8402],{"class":81,"line":103},[79,8403,107],{"emptyLinePlaceholder":106},[79,8405,8406,8408,8411,8413,8415,8418,8420,8423,8425,8428,8430],{"class":81,"line":110},[79,8407,4040],{"class":85},[79,8409,8410],{"class":133}," bot",[79,8412,307],{"class":85},[79,8414,310],{"class":85},[79,8416,8417],{"class":116}," TelegramBot",[79,8419,4053],{"class":89},[79,8421,8422],{"class":133},"TELEGRAM_BOT_TOKEN",[79,8424,358],{"class":85},[79,8426,8427],{"class":89},", { polling: ",[79,8429,817],{"class":133},[79,8431,5458],{"class":89},[79,8433,8434],{"class":81,"line":123},[79,8435,107],{"emptyLinePlaceholder":106},[79,8437,8438,8441,8444,8446,8449,8451,8453,8455,8458,8460,8462],{"class":81,"line":139},[79,8439,8440],{"class":89},"bot.",[79,8442,8443],{"class":116},"on",[79,8445,269],{"class":89},[79,8447,8448],{"class":96},"'message'",[79,8450,279],{"class":89},[79,8452,5540],{"class":85},[79,8454,355],{"class":89},[79,8456,8457],{"class":126},"msg",[79,8459,5017],{"class":89},[79,8461,1023],{"class":85},[79,8463,120],{"class":89},[79,8465,8466,8468,8471,8473],{"class":81,"line":152},[79,8467,301],{"class":85},[79,8469,8470],{"class":133}," chatId",[79,8472,307],{"class":85},[79,8474,8475],{"class":89}," msg.chat.id;\n",[79,8477,8478,8480,8483,8485],{"class":81,"line":165},[79,8479,301],{"class":85},[79,8481,8482],{"class":133}," userText",[79,8484,307],{"class":85},[79,8486,8487],{"class":89}," msg.text;\n",[79,8489,8490],{"class":81,"line":177},[79,8491,107],{"emptyLinePlaceholder":106},[79,8493,8494,8496,8498,8500,8503,8505],{"class":81,"line":189},[79,8495,352],{"class":85},[79,8497,355],{"class":89},[79,8499,358],{"class":85},[79,8501,8502],{"class":89},"userText) ",[79,8504,5827],{"class":85},[79,8506,100],{"class":89},[79,8508,8509],{"class":81,"line":202},[79,8510,107],{"emptyLinePlaceholder":106},[79,8512,8513],{"class":81,"line":208},[79,8514,8515],{"class":393},"  \u002F\u002F 1. Получаем статус сессии из БД\n",[79,8517,8518,8520,8522,8524,8526,8528,8531],{"class":81,"line":213},[79,8519,301],{"class":85},[79,8521,4175],{"class":133},[79,8523,307],{"class":85},[79,8525,4180],{"class":85},[79,8527,5701],{"class":89},[79,8529,8530],{"class":116},"getChatSession",[79,8532,8533],{"class":89},"(chatId);\n",[79,8535,8536],{"class":81,"line":223},[79,8537,107],{"emptyLinePlaceholder":106},[79,8539,8540],{"class":81,"line":235},[79,8541,8542],{"class":393},"  \u002F\u002F Если чат переключен на человека, пересылаем сообщения в Slack\u002Fпанель менеджеров\n",[79,8544,8545,8547,8550,8552,8555],{"class":81,"line":247},[79,8546,352],{"class":85},[79,8548,8549],{"class":89}," (session?.status ",[79,8551,692],{"class":85},[79,8553,8554],{"class":96}," 'human_handling'",[79,8556,5161],{"class":89},[79,8558,8559,8561,8564],{"class":81,"line":252},[79,8560,6539],{"class":85},[79,8562,8563],{"class":116}," forwardToManagers",[79,8565,8566],{"class":89},"(chatId, msg);\n",[79,8568,8569,8571],{"class":81,"line":257},[79,8570,367],{"class":85},[79,8572,100],{"class":89},[79,8574,8575],{"class":81,"line":298},[79,8576,382],{"class":89},[79,8578,8579],{"class":81,"line":319},[79,8580,107],{"emptyLinePlaceholder":106},[79,8582,8583],{"class":81,"line":343},[79,8584,8585],{"class":393},"  \u002F\u002F 2. Выполняем RAG-поиск\n",[79,8587,8588,8590],{"class":81,"line":349},[79,8589,745],{"class":85},[79,8591,120],{"class":89},[79,8593,8594,8596,8599,8601,8603,8605],{"class":81,"line":364},[79,8595,753],{"class":85},[79,8597,8598],{"class":133}," context",[79,8600,307],{"class":85},[79,8602,4180],{"class":85},[79,8604,7906],{"class":116},[79,8606,8607],{"class":89},"(userText);\n",[79,8609,8610,8612,8615,8617,8619,8621],{"class":81,"line":379},[79,8611,753],{"class":85},[79,8613,8614],{"class":133}," aiResponse",[79,8616,307],{"class":85},[79,8618,4180],{"class":85},[79,8620,8133],{"class":116},[79,8622,8623],{"class":89},"(userText, context);\n",[79,8625,8626],{"class":81,"line":385},[79,8627,107],{"emptyLinePlaceholder":106},[79,8629,8630],{"class":81,"line":390},[79,8631,8632],{"class":393},"    \u002F\u002F 3. Проверка на эскалацию\n",[79,8634,8635,8637,8640,8643,8646,8648,8650],{"class":81,"line":397},[79,8636,1051],{"class":85},[79,8638,8639],{"class":89}," (aiResponse.",[79,8641,8642],{"class":116},"trim",[79,8644,8645],{"class":89},"() ",[79,8647,692],{"class":85},[79,8649,8181],{"class":96},[79,8651,5161],{"class":89},[79,8653,8654],{"class":81,"line":450},[79,8655,8656],{"class":393},"      \u002F\u002F Обновляем статус сессии в базе данных\n",[79,8658,8659,8662,8664,8667,8670,8673],{"class":81,"line":455},[79,8660,8661],{"class":85},"      await",[79,8663,5701],{"class":89},[79,8665,8666],{"class":116},"updateChatSession",[79,8668,8669],{"class":89},"(chatId, { status: ",[79,8671,8672],{"class":96},"'human_handling'",[79,8674,5458],{"class":89},[79,8676,8677],{"class":81,"line":461},[79,8678,1082],{"class":89},[79,8680,8681],{"class":81,"line":474},[79,8682,8683],{"class":393},"      \u002F\u002F Оповещаем менеджеров через вебхук\n",[79,8685,8686,8688,8691],{"class":81,"line":516},[79,8687,8661],{"class":85},[79,8689,8690],{"class":116}," notifySupportStaff",[79,8692,8693],{"class":89},"(chatId, userText);\n",[79,8695,8696],{"class":81,"line":536},[79,8697,1082],{"class":89},[79,8699,8700,8702,8705,8708],{"class":81,"line":541},[79,8701,8661],{"class":85},[79,8703,8704],{"class":89}," bot.",[79,8706,8707],{"class":116},"sendMessage",[79,8709,4957],{"class":89},[79,8711,8712],{"class":81,"line":547},[79,8713,8714],{"class":89},"        chatId, \n",[79,8716,8717],{"class":81,"line":553},[79,8718,8719],{"class":96},"        \"К сожалению, я не нашел ответ в документации. Я переключил этот чат на службу поддержки. Специалист ответит вам в ближайшее время.\"\n",[79,8721,8722],{"class":81,"line":566},[79,8723,8724],{"class":89},"      );\n",[79,8726,8727,8729],{"class":81,"line":586},[79,8728,5060],{"class":85},[79,8730,100],{"class":89},[79,8732,8733],{"class":81,"line":597},[79,8734,1190],{"class":89},[79,8736,8737],{"class":81,"line":607},[79,8738,107],{"emptyLinePlaceholder":106},[79,8740,8741],{"class":81,"line":612},[79,8742,8743],{"class":393},"    \u002F\u002F 4. Отправляем ИИ-ответ пользователю\n",[79,8745,8746,8748,8750,8752],{"class":81,"line":618},[79,8747,6539],{"class":85},[79,8749,8704],{"class":89},[79,8751,8707],{"class":116},[79,8753,8754],{"class":89},"(chatId, aiResponse);\n",[79,8756,8757],{"class":81,"line":630},[79,8758,107],{"emptyLinePlaceholder":106},[79,8760,8761,8763,8765],{"class":81,"line":644},[79,8762,826],{"class":89},[79,8764,829],{"class":85},[79,8766,832],{"class":89},[79,8768,8769,8772,8774,8776,8779],{"class":81,"line":654},[79,8770,8771],{"class":89},"    console.",[79,8773,5169],{"class":116},[79,8775,269],{"class":89},[79,8777,8778],{"class":96},"'Ошибка в цикле RAG:'",[79,8780,8781],{"class":89},", error);\n",[79,8783,8784,8786,8788,8790,8793,8796],{"class":81,"line":668},[79,8785,6539],{"class":85},[79,8787,8704],{"class":89},[79,8789,8707],{"class":116},[79,8791,8792],{"class":89},"(chatId, ",[79,8794,8795],{"class":96},"\"Произошла ошибка. Перевожу диалог на оператора.\"",[79,8797,340],{"class":89},[79,8799,8800,8802,8804,8806,8808,8810],{"class":81,"line":673},[79,8801,6539],{"class":85},[79,8803,5701],{"class":89},[79,8805,8666],{"class":116},[79,8807,8669],{"class":89},[79,8809,8672],{"class":96},[79,8811,5458],{"class":89},[79,8813,8814],{"class":81,"line":679},[79,8815,382],{"class":89},[79,8817,8818],{"class":81,"line":698},[79,8819,3471],{"class":89},[11,8821,8822,8823,8825],{},"В моем проекте ",[1270,8824,1273],{"href":1272}," мы создали аналогичную RAG-систему для обработки частых вопросов пользователей по настройкам ботов и платежам. Если пользователь задает сложный технический вопрос, на который нет готового ответа, сессия автоматически переводится на панель оператора.",[11,8827,8828,8829,8833,8834,60],{},"Этот процесс эскалации становится еще эффективнее, если связать его с ",[1270,8830,8832],{"href":8831},"\u002Fru\u002Fblog\u002Ftelegram-bot-crm-integration","Интеграцией Telegram-ботов с CRM"," для автоматического логирования событий. А саму панель общения оператора с клиентами можно удобно разместить прямо внутри ",[1270,8835,8836],{"href":6019},"Telegram Web App",[26,8838,1290],{"id":1289},[1292,8840,8841,8849,8856],{},[1232,8842,8843,8848],{},[1270,8844,8847],{"href":8845,"rel":8846},"https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Fembeddings",[1300],"OpenAI Embeddings"," — генерация векторов, модель text-embedding-3-small из примера",[1232,8850,8851,8855],{},[1270,8852,7510],{"href":8853,"rel":8854},"https:\u002F\u002Fgithub.com\u002Fpgvector\u002Fpgvector",[1300]," — расширение PostgreSQL для векторного поиска",[1232,8857,8858,8863],{},[1270,8859,8862],{"href":8860,"rel":8861},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fapi",[1300],"Telegram Bot API"," — приём сообщений и long polling",[11,8865,8866],{},"Внедрение RAG-архитектуры с автоматическим переключением на оператора кратно снижает затраты на поддержку: типовые вопросы закрывает ИИ, а люди подключаются только к сложным диалогам — без потери качества сервиса.",[11,8868,8869,8870,1321,8873,60],{},"Если вы хотите разработать умного ИИ-бота для техподдержки, настроить векторный поиск Pgvector или интегрировать чаты с AmoCRM\u002FBitrix24, ознакомьтесь с моей услугой ",[1270,8871,8872],{"href":1319},"Создания Telegram-ботов под ключ",[1270,8874,1325],{"href":1324},[1328,8876,8877],{},"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);}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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":40,"searchDepth":103,"depth":103,"links":8879},[8880,8881,8882,8883,8884,8885],{"id":7514,"depth":103,"text":7515},{"id":7524,"depth":103,"text":7525},{"id":7695,"depth":103,"text":7696},{"id":8113,"depth":103,"text":8114},{"id":8372,"depth":103,"text":8373},{"id":1289,"depth":103,"text":1290},"2026-06-04","Техническое руководство по проектированию бота поддержки в Telegram с использованием OpenAI Embeddings, PostgreSQL Pgvector и гибридной системы эскалации на оператора.",[8889,8892,8895],{"q":8890,"a":8891},"Чем RAG-бот отличается от «просто ChatGPT в боте»?","RAG-бот отвечает строго по вашей базе знаний: сначала находит релевантные фрагменты документации через векторный поиск, затем передаёт их модели как контекст. Это устраняет главные проблемы «голой» LLM — галлюцинации и незнание ваших цен, инструкций и API.",{"q":8893,"a":8894},"Что происходит, если бот не знает ответа?","Срабатывает протокол эскалации: если векторный поиск не нашёл релевантного контекста или модель вернула маркер неуверенности, сессия помечается как human_handling, менеджеры получают уведомление, а все следующие сообщения клиента пересылаются живому оператору.",{"q":8896,"a":8897},"Сколько стоит эксплуатация такого бота?","Две статьи расходов: генерация эмбеддингов при индексации базы знаний (разовая и копеечная) и вызовы LLM на каждый вопрос — оплата по фактическому использованию API. На типовом потоке вопросов это заметно дешевле ставки оператора поддержки.","\u002Fimages\u002Fblog\u002Fblog_ai_rag_support.jpg",{},"\u002Fblog\u002Fru\u002Ftelegram-bot-ai-rag-support",{"title":7485,"description":8887},"blog\u002Fru\u002Ftelegram-bot-ai-rag-support",[1362,2946,8904,8905,1365,6802],"RAG","LLM","225RZ1ngfAy8AELI0CQam9IZb61lmQOrlgyE7TxMJJI",{"id":8908,"title":8909,"body":8910,"date":9181,"description":9182,"extension":1344,"faq":9183,"image":9196,"lang":1356,"meta":9197,"navigation":106,"path":9199,"published":106,"readTime":152,"seo":9200,"stem":9201,"tags":9202,"updated":1342,"__hash__":9206},"blog\u002Fblog\u002Fru\u002Fbot-builder-vs-custom-development.md","Конструктор ботов или кастомная разработка: как выбрать в 2026 году",{"type":8,"value":8911,"toc":9175},[8912,8915,8925,8929,9050,9054,9057,9083,9086,9090,9093,9131,9139,9143,9146,9157,9164],[11,8913,8914],{},"Короткий ответ: если ваш сценарий укладывается в типовые блоки — запись, оплата, меню, рассылки, — берите no-code конструктор и запускайтесь за день. Если нужна нестандартная логика, глубокие интеграции или высокая нагрузка — заказывайте кастомную разработку от $250. Ниже — таблица решений и разбор, где проходит граница.",[11,8916,8917,8918,8920,8921,8924],{},"Я в необычной позиции для этого сравнения: я одновременно автор no-code конструктора ",[1270,8919,1273],{"href":1272}," и разработчик ",[1270,8922,8923],{"href":1319},"кастомных Telegram-ботов на заказ",". Мне невыгодно приукрашивать ни один из вариантов — расскажу, как выбираю сам.",[26,8926,8928],{"id":8927},"конструктор-против-кастома-таблица-решений","Конструктор против кастома: таблица решений",[2415,8930,8931,8943],{},[2418,8932,8933],{},[2421,8934,8935,8937,8940],{},[2424,8936,6130],{"align":2426},[2424,8938,8939],{"align":2426},"No-code конструктор",[2424,8941,8942],{"align":2426},"Кастомная разработка",[2435,8944,8945,8958,8971,8985,8998,9011,9024,9037],{},[2421,8946,8947,8952,8955],{},[2440,8948,8949],{"align":2426},[15,8950,8951],{},"Срок запуска",[2440,8953,8954],{"align":2426},"Часы — 1 день",[2440,8956,8957],{"align":2426},"3–14 дней",[2421,8959,8960,8965,8968],{},[2440,8961,8962],{"align":2426},[15,8963,8964],{},"Стартовая цена",[2440,8966,8967],{"align":2426},"Подписка от нескольких $ в месяц",[2440,8969,8970],{"align":2426},"От $250 разово",[2421,8972,8973,8979,8982],{},[2440,8974,8975,8978],{"align":2426},[15,8976,8977],{},"Типовые сценарии"," (запись, оплата, FAQ)",[2440,8980,8981],{"align":2426},"✅ Готовые блоки",[2440,8983,8984],{"align":2426},"✅ Но дороже, чем нужно",[2421,8986,8987,8992,8995],{},[2440,8988,8989],{"align":2426},[15,8990,8991],{},"Нестандартная логика",[2440,8993,8994],{"align":2426},"❌ Упрётесь в ограничения блоков",[2440,8996,8997],{"align":2426},"✅ Любая",[2421,8999,9000,9005,9008],{},[2440,9001,9002],{"align":2426},[15,9003,9004],{},"Интеграции",[2440,9006,9007],{"align":2426},"Популярные из коробки (CRM, таблицы, платежи)",[2440,9009,9010],{"align":2426},"Любые, включая внутренние системы",[2421,9012,9013,9018,9021],{},[2440,9014,9015],{"align":2426},[15,9016,9017],{},"Высокая нагрузка",[2440,9019,9020],{"align":2426},"Зависит от платформы",[2440,9022,9023],{"align":2426},"✅ Архитектура под задачу",[2421,9025,9026,9031,9034],{},[2440,9027,9028],{"align":2426},[15,9029,9030],{},"Правки без программиста",[2440,9032,9033],{"align":2426},"✅ Всё редактируете сами",[2440,9035,9036],{"align":2426},"✅ Контент — сами, логика — через разработчика",[2421,9038,9039,9044,9047],{},[2440,9040,9041],{"align":2426},[15,9042,9043],{},"Владение",[2440,9045,9046],{"align":2426},"Бот живёт на платформе",[2440,9048,9049],{"align":2426},"Код и данные полностью ваши",[26,9051,9053],{"id":9052},"когда-конструктора-достаточно","Когда конструктора достаточно",[11,9055,9056],{},"По моему опыту с TeleGo, типовые блоки закрывают большинство задач малого бизнеса:",[1292,9058,9059,9065,9071,9077],{},[1232,9060,9061,9064],{},[15,9062,9063],{},"Запись клиентов"," — салоны, студии, частная практика;",[1232,9066,9067,9070],{},[15,9068,9069],{},"Продажа доступов и курсов"," — оплата через Stripe и Telegram Stars внутри чата;",[1232,9072,9073,9076],{},[15,9074,9075],{},"Сбор и прогрев заявок"," — анкеты, сегментация, автонапоминания;",[1232,9078,9079,9082],{},[15,9080,9081],{},"Поддержка"," — ответы на частые вопросы, в том числе ИИ-ответами по вашей базе знаний.",[11,9084,9085],{},"Если ваша задача в этом списке — не переплачивайте за разработку: соберите бота в визуальном редакторе, проверьте спрос, и только потом решайте, нужен ли кастом. В TeleGo для этого есть бесплатный 7-дневный Pro-период.",[26,9087,9089],{"id":9088},"когда-нужен-кастом","Когда нужен кастом",[11,9091,9092],{},"Кастомная разработка оправдана, когда появляется хотя бы одно из:",[1229,9094,9095,9104,9110,9119,9125],{},[1232,9096,9097,9100,9101,60],{},[15,9098,9099],{},"Нестандартная бизнес-логика"," — скоринг лидов, сложные ветвления, расчёты. Пример с кодом — в моей статье про ",[1270,9102,9103],{"href":8831},"интеграцию бота с amoCRM",[1232,9105,9106,9109],{},[15,9107,9108],{},"Глубокие интеграции"," — внутренняя CRM, 1С, специфические API, которых нет в конструкторах.",[1232,9111,9112,9115,9116,441],{},[15,9113,9114],{},"ИИ с вашими данными"," — RAG-бот, который отвечает строго по базе знаний и передаёт сложные диалоги оператору (",[1270,9117,9118],{"href":1277},"разбор архитектуры",[1232,9120,9121,9124],{},[15,9122,9123],{},"Нагрузка"," — десятки тысяч пользователей требуют своей архитектуры очередей и базы данных.",[1232,9126,9127,9130],{},[15,9128,9129],{},"Требование владеть кодом"," — корпоративные политики или инвесторы.",[11,9132,9133,9134,9138],{},"Сколько это стоит по типам ботов — в ",[1270,9135,9137],{"href":9136},"\u002Fru\u002Fblog\u002Ftelegram-bot-development-cost","подробной смете разработки","; краткая версия: простой бот от $250, бизнес-бот от $500, ИИ-бот от $900, всё с панелью управления и 6 месяцами хостинга.",[26,9140,9142],{"id":9141},"гибридная-траектория-как-делаю-я","Гибридная траектория: как делаю я",[11,9144,9145],{},"Самая частая ошибка — выбирать «навсегда». Рабочая схема выглядит иначе:",[1229,9147,9148,9151,9154],{},[1232,9149,9150],{},"Соберите MVP-бота на конструкторе за день и запустите на реальных клиентах.",[1232,9152,9153],{},"Соберите статистику: какие сценарии используются, где пользователи отваливаются.",[1232,9155,9156],{},"Упёрлись в ограничения — заказывайте кастом, перенося уже проверенные сценарии, а не гипотезы.",[11,9158,9159,9160,9163],{},"Так вы платите за разработку один раз и за подтверждённую спросом логику. Технически бот работает через ",[1270,9161,8862],{"href":8860,"rel":9162},[1300]," в обоих случаях — миграция это перенос сценариев и базы клиентов, а не начало с нуля.",[11,9165,9166,9167,9170,9171,9174],{},"Сомневаетесь, какой путь ваш? Опишите задачу на странице ",[1270,9168,9169],{"href":1319},"разработки Telegram-ботов"," — я честно скажу, если вам хватит конструктора, или приходите на ",[1270,9172,9173],{"href":1324},"консультацию"," за разбором сценария.",{"title":40,"searchDepth":103,"depth":103,"links":9176},[9177,9178,9179,9180],{"id":8927,"depth":103,"text":8928},{"id":9052,"depth":103,"text":9053},{"id":9088,"depth":103,"text":9089},{"id":9141,"depth":103,"text":9142},"2026-05-31","Честное сравнение no-code конструкторов Telegram-ботов и заказной разработки — по цене, срокам, гибкости и потолку возможностей. Таблица решений внутри.",[9184,9187,9190,9193],{"q":9185,"a":9186},"Когда конструктора ботов достаточно?","Когда сценарий укладывается в типовые блоки: приветствие, меню, запись, приём оплаты, рассылки, ответы на частые вопросы. Это 80% задач малого бизнеса — салоны, школы, консультанты, магазины.",{"q":9188,"a":9189},"Можно ли потом переехать с конструктора на кастомную разработку?","Да, и это нормальная траектория: проверяете спрос на конструкторе за день, а когда упираетесь в ограничения — заказываете кастомного бота, перенося проверенные сценарии. Главное — заранее выгрузить базу клиентов.",{"q":9191,"a":9192},"Что дешевле на горизонте года?","Для типовых сценариев — конструктор: подписка стоит меньше, чем поддержка кастомного кода. Для нестандартной логики — кастом: попытки обойти ограничения конструктора костылями обходятся дороже разработки.",{"q":9194,"a":9195},"Кто поддерживает бота после запуска?","В конструкторе — вы сами через визуальный редактор. В кастомной разработке — вы редактируете контент через панель управления, а логику и интеграции поддерживает разработчик.","\u002Fimages\u002Fblog\u002Fblog_bot_builder_vs_custom.jpg",{"robots":9198},"noindex, nofollow","\u002Fblog\u002Fru\u002Fbot-builder-vs-custom-development",{"title":8909,"description":9182},"blog\u002Fru\u002Fbot-builder-vs-custom-development",[1362,9203,9204,9205],"Боты","No-code","Бизнес","JEkpgA6VG-5E7LEfnV9PNxIU0Fe-lMe9104ev8uMFKw",{"id":9208,"title":9209,"body":9210,"date":9460,"description":9461,"extension":1344,"faq":9462,"image":9475,"lang":1356,"meta":9476,"navigation":106,"path":9477,"published":106,"readTime":165,"seo":9478,"stem":9479,"tags":9480,"updated":1342,"__hash__":9481},"blog\u002Fblog\u002Fru\u002Ftelegram-bot-development-cost.md","Сколько стоит разработка Telegram-бота в 2026 году: смета по типам ботов",{"type":8,"value":9211,"toc":9453},[9212,9215,9219,9227,9252,9256,9259,9327,9330,9334,9337,9363,9367,9370,9406,9409,9413,9437,9444],[11,9213,9214],{},"Разработка Telegram-бота под ключ стоит от $250 за простого бота с меню и сбором заявок до $900+ за умного бота с ИИ-ответами и интеграцией с CRM. Итоговая цена зависит от трёх вещей: сложности сценариев, количества интеграций и требований к нагрузке. В этой статье — честная смета по типам ботов, что именно входит в стоимость и какие расходы появляются после запуска.",[26,9216,9218],{"id":9217},"от-чего-зависит-стоимость-telegram-бота","От чего зависит стоимость Telegram-бота?",[11,9220,9221,9222,9226],{},"Цена бота — это почти всегда цена его логики, а не «кода отправки сообщений». Сам Telegram Bot API бесплатен и хорошо документирован (",[1270,9223,9225],{"href":8860,"rel":9224},[1300],"официальная документация","), поэтому бюджет расходуется на:",[1229,9228,9229,9235,9240,9246],{},[1232,9230,9231,9234],{},[15,9232,9233],{},"Сценарии"," — сколько веток диалога, состояний и исключений нужно спроектировать и протестировать.",[1232,9236,9237,9239],{},[15,9238,9004],{}," — платежи (Stripe, Telegram Stars, криптовалюта), CRM (amoCRM, Bitrix24), таблицы и календари Google, ИИ-модели.",[1232,9241,9242,9245],{},[15,9243,9244],{},"Панель управления"," — сможет ли владелец менять контент сам или каждый чих потребует разработчика.",[1232,9247,9248,9251],{},[15,9249,9250],{},"Нагрузку"," — бот для салона на 200 клиентов и бот для онлайн-школы на 20 000 студентов устроены по-разному.",[26,9253,9255],{"id":9254},"цены-по-типам-ботов","Цены по типам ботов",[11,9257,9258],{},"Смета из моей реальной практики — это стартовые цены при типовом объёме работ:",[2415,9260,9261,9277],{},[2418,9262,9263],{},[2421,9264,9265,9268,9271,9274],{},[2424,9266,9267],{"align":2426},"Тип бота",[2424,9269,9270],{"align":2426},"Что умеет",[2424,9272,9273],{"align":2426},"Срок",[2424,9275,9276],{"align":2426},"Цена",[2435,9278,9279,9295,9311],{},[2421,9280,9281,9286,9289,9292],{},[2440,9282,9283],{"align":2426},[15,9284,9285],{},"Простой бот",[2440,9287,9288],{"align":2426},"Приветственное меню, кнопки-навигация, сбор контактов, база заявок",[2440,9290,9291],{"align":2426},"3–5 дней",[2440,9293,9294],{"align":2426},"от $250",[2421,9296,9297,9302,9305,9308],{},[2440,9298,9299],{"align":2426},[15,9300,9301],{},"Бизнес-бот",[2440,9303,9304],{"align":2426},"Всё из простого + онлайн-запись или оплата, анкеты, автонапоминания, сегментация клиентов",[2440,9306,9307],{"align":2426},"7–10 дней",[2440,9309,9310],{"align":2426},"от $500",[2421,9312,9313,9318,9321,9324],{},[2440,9314,9315],{"align":2426},[15,9316,9317],{},"Умный бот с ИИ",[2440,9319,9320],{"align":2426},"Всё из бизнес-бота + автоответы по вашей базе знаний, интеграция с CRM и внешними системами, передача диалога человеку",[2440,9322,9323],{"align":2426},"10–14 дней",[2440,9325,9326],{"align":2426},"от $900",[11,9328,9329],{},"Во все тарифы входит: настройка бота, панель управления без кода, тестирование, запуск и первые 6 месяцев хостинга.",[26,9331,9333],{"id":9332},"какие-расходы-появляются-после-запуска","Какие расходы появляются после запуска?",[11,9335,9336],{},"Честный ответ, о котором редко пишут в прайсах:",[1292,9338,9339,9345,9351,9357],{},[1232,9340,9341,9344],{},[15,9342,9343],{},"Хостинг"," — после включённого периода простой бот живёт на небольшом VPS; это единицы долларов в месяц.",[1232,9346,9347,9350],{},[15,9348,9349],{},"API языковых моделей"," — для ИИ-ботов ответы генерируются через OpenAI, Claude или Gemini, и оплата идёт по фактическому использованию. На типовом боте поддержки это заметно меньше зарплаты оператора, но в смете эту строку нужно видеть заранее.",[1232,9352,9353,9356],{},[15,9354,9355],{},"Комиссии платёжных систем"," — Stripe и Telegram Stars берут процент с транзакций; это расход не на бота, а на приём платежей как таковой.",[1232,9358,9359,9362],{},[15,9360,9361],{},"Доработки"," — новые ветки сценариев и интеграции. Именно здесь окупается панель управления: правки текстов и кнопок вы делаете сами и бесплатно.",[26,9364,9366],{"id":9365},"когда-бот-окупается","Когда бот окупается?",[11,9368,9369],{},"Экономика канала — главная причина, почему бизнес заказывает ботов, а не ещё один лендинг:",[1292,9371,9372,9383,9394],{},[1232,9373,9374,9375,355,9378,3679],{},"у Telegram больше ",[15,9376,9377],{},"1 миллиарда активных пользователей в месяц",[1270,9379,9382],{"href":9380,"rel":9381},"https:\u002F\u002Ftechcrunch.com\u002F2025\u002F03\u002F19\u002Ftelegram-founder-pavel-durov-says-app-now-has-1b-users-calls-whatsapp-a-cheap-watered-down-imitation\u002F",[1300],"TechCrunch, март 2025",[1232,9384,9385,9386,355,9389,3679],{},"открываемость сообщений в мессенджерах достигает ",[15,9387,9388],{},"80% против ~20% у email",[1270,9390,9393],{"href":9391,"rel":9392},"https:\u002F\u002Fwww.levelingup.com\u002Fgrowth-everywhere-interview\u002Fmikael-yang-manychat\u002F",[1300],"интервью CEO ManyChat",[1232,9395,9396,9397,9400,9401,441],{},"стоимость лида через бота может быть ",[15,9398,9399],{},"до 7 раз ниже",", чем через классический лендинг (",[1270,9402,9405],{"href":9403,"rel":9404},"https:\u002F\u002Fblog.chatfuel.com\u002Fchatbots-vs-landing-pages\u002F",[1300],"данные Chatfuel",[11,9407,9408],{},"Практический ориентир: если бот экономит хотя бы час ручной работы администратора в день или приносит несколько заявок в неделю, вложение в $250–900 возвращается за первые месяцы.",[26,9410,9412],{"id":9411},"как-сэкономить-без-потери-качества","Как сэкономить без потери качества?",[1292,9414,9415,9421,9431],{},[1232,9416,9417,9420],{},[15,9418,9419],{},"Начните с простого бота"," и добавляйте ИИ и интеграции после проверки спроса — архитектура моих ботов позволяет наращивать функции без переписывания.",[1232,9422,9423,9426,9427,60],{},[15,9424,9425],{},"Подготовьте сценарий заранее",": описанные своими словами диалоги и список вопросов клиентов сокращают этап проектирования. Что должно быть в таком описании — тема отдельного ",[1270,9428,9430],{"href":9429},"\u002Fru\u002Fblog\u002Ftelegram-bot-specification-checklist","чек-листа по ТЗ на бота",[1232,9432,9433,9436],{},[15,9434,9435],{},"Не заказывайте функции «на вырост»",": рассылки, реферальные программы и мультиязычность легко добавить вторым этапом.",[11,9438,9439,9440,60],{},"Если вы выбираете между заказной разработкой и no-code конструктором — я разобрал этот выбор отдельно в статье ",[1270,9441,9443],{"href":9442},"\u002Fru\u002Fblog\u002Fbot-builder-vs-custom-development","«Конструктор ботов или кастомная разработка»",[11,9445,9446,9447,9449,9450,9452],{},"Хотите точную смету под вашу задачу? Опишите её на странице ",[1270,9448,9169],{"href":1319}," — я отвечу в течение 24 часов с оценкой сроков и бюджета, или приходите на ",[1270,9451,9173],{"href":1324},", если задача пока не оформилась в ТЗ.",{"title":40,"searchDepth":103,"depth":103,"links":9454},[9455,9456,9457,9458,9459],{"id":9217,"depth":103,"text":9218},{"id":9254,"depth":103,"text":9255},{"id":9332,"depth":103,"text":9333},{"id":9365,"depth":103,"text":9366},{"id":9411,"depth":103,"text":9412},"2026-05-28","Реальные цены на разработку Telegram-ботов — от простого меню за $250 до ИИ-бота с CRM. Что входит в стоимость, скрытые расходы и когда бот окупается.",[9463,9466,9469,9472],{"q":9464,"a":9465},"Сколько времени занимает разработка Telegram-бота?","Простой бот с меню и сбором заявок — 3–5 дней. Бизнес-бот с записью и оплатой — 7–10 дней. Умный бот с ИИ-ответами и интеграцией CRM — 10–14 дней. Сроки указаны от согласования технического задания до запуска.",{"q":9467,"a":9468},"Нужно ли платить за хостинг бота отдельно?","В моих тарифах первые 6 месяцев хостинга включены в стоимость разработки. Дальше содержание простого бота обходится в стоимость небольшого VPS; для ботов с ИИ добавляется оплата API языковой модели по фактическому использованию.",{"q":9470,"a":9471},"Смогу ли я менять тексты и сценарии бота без программиста?","Да. Каждый бот сдаётся с панелью управления: тексты, кнопки, сценарии и рассылки редактируются визуально, без кода. Программист нужен только для новых интеграций или нестандартной логики.",{"q":9473,"a":9474},"Из-за чего цена бота может вырасти относительно базового тарифа?","Три главных фактора: нестандартные интеграции (внутренние CRM, специфические платёжные провайдеры), сложная ветвящаяся логика сценариев и требования к нагрузке — боту на десятки тысяч пользователей нужна другая архитектура очередей и базы данных.","\u002Fimages\u002Fblog\u002Fblog_telegram_bot_cost.jpg",{},"\u002Fblog\u002Fru\u002Ftelegram-bot-development-cost",{"title":9209,"description":9461},"blog\u002Fru\u002Ftelegram-bot-development-cost",[1362,9203,3813,9205],"oSmgENBMu-gbsMPe1_eNxU4OLay9VIBuLmLm0yk5XTg",{"id":9483,"title":9484,"body":9485,"date":9730,"description":9731,"extension":1344,"faq":9732,"image":9745,"lang":1356,"meta":9746,"navigation":106,"path":9747,"published":106,"readTime":152,"seo":9748,"stem":9749,"tags":9750,"updated":1342,"__hash__":9753},"blog\u002Fblog\u002Fru\u002Ftelegram-bot-specification-checklist.md","Что должно быть в ТЗ на Telegram-бота: чек-лист с примерами",{"type":8,"value":9486,"toc":9721},[9487,9490,9494,9497,9511,9514,9518,9521,9527,9530,9534,9537,9575,9579,9582,9632,9638,9642,9665,9669,9672,9701,9705,9712],[11,9488,9489],{},"Хорошее описание задачи сокращает и смету, и сроки разработки Telegram-бота: чем меньше разработчик додумывает, тем меньше закладывает на риски. Ниже — чек-лист из 6 блоков, который я прошу заполнить клиентов перед оценкой проекта. Он же — фильтр от типовой ошибки: заказать «бота вообще» вместо бота под конкретный процесс.",[26,9491,9493],{"id":9492},"_1-цель-какую-метрику-меняет-бот","1. Цель: какую метрику меняет бот",[11,9495,9496],{},"Один абзац, без которого всё остальное бессмысленно:",[1292,9498,9499,9505],{},[1232,9500,9501,9504],{},[2410,9502,9503],{},"Плохо",": «Нужен бот для салона».",[1232,9506,9507,9510],{},[2410,9508,9509],{},"Хорошо",": «Клиенты записываются по телефону, администратор тратит на это 3 часа в день. Бот должен принимать 70% записей без участия человека».",[11,9512,9513],{},"Из цели вытекает и приоритет функций, и то, как мы поймём, что бот окупился.",[26,9515,9517],{"id":9516},"_2-сценарии-диалогов-по-шагам","2. Сценарии диалогов по шагам",[11,9519,9520],{},"Опишите 3–5 основных сценариев так, как их пройдёт живой клиент:",[34,9522,9525],{"className":9523,"code":9524,"language":39,"meta":40},[37],"Сценарий «Запись на услугу»:\n1. Клиент нажимает \u002Fstart → приветствие + меню [Записаться] [Цены] [Вопрос]\n2. [Записаться] → бот показывает список услуг (кнопки)\n3. Выбор услуги → бот показывает свободные слоты на неделю\n4. Выбор слота → бот просит телефон (кнопка «Поделиться контактом»)\n5. Подтверждение → запись в календарь + уведомление администратору\n6. За день до визита → автонапоминание клиенту\n",[42,9526,9524],{"__ignoreMap":40},[11,9528,9529],{},"Такой формат отвечает на большинство вопросов проектирования: какие кнопки, какие данные собираем, кого уведомляем.",[26,9531,9533],{"id":9532},"_3-граничные-случаи-и-ошибки","3. Граничные случаи и ошибки",[11,9535,9536],{},"Блок, который забывают в 9 из 10 брифов, а он определяет качество бота:",[1292,9538,9539,9545,9552,9562,9569],{},[1232,9540,9541,9542,789],{},"Что видит клиент, если ",[15,9543,9544],{},"все слоты заняты",[1232,9546,9547,9548,9551],{},"Что происходит, если клиент ",[15,9549,9550],{},"бросил диалог на середине"," и вернулся через день?",[1232,9553,9554,9555,9558,9559,289],{},"Кто отвечает, если клиент задаёт ",[15,9556,9557],{},"вопрос не по сценарию","? (вариант с ИИ-ответами по базе знаний я разбирал в статье про ",[1270,9560,9561],{"href":1277},"RAG-бота поддержки",[1232,9563,9564,9565,9568],{},"Как клиент ",[15,9566,9567],{},"отменяет или переносит"," запись?",[1232,9570,9571,9572,789],{},"Что делает бот при ",[15,9573,9574],{},"неудачной оплате",[26,9576,9578],{"id":9577},"_4-интеграции-и-данные","4. Интеграции и данные",[11,9580,9581],{},"Перечислите системы, с которыми бот должен обмениваться данными, и в какую сторону:",[2415,9583,9584,9597],{},[2418,9585,9586],{},[2421,9587,9588,9591,9594],{},[2424,9589,9590],{"align":2426},"Система",[2424,9592,9593],{"align":2426},"Что передаём",[2424,9595,9596],{"align":2426},"Что получаем",[2435,9598,9599,9610,9621],{},[2421,9600,9601,9604,9607],{},[2440,9602,9603],{"align":2426},"CRM (amoCRM, Bitrix24…)",[2440,9605,9606],{"align":2426},"Контакт, сделка, ответы анкеты",[2440,9608,9609],{"align":2426},"Статусы, история клиента",[2421,9611,9612,9615,9618],{},[2440,9613,9614],{"align":2426},"Платежи (Stripe, Telegram Stars)",[2440,9616,9617],{"align":2426},"Счёт на оплату",[2440,9619,9620],{"align":2426},"Статус транзакции",[2421,9622,9623,9626,9629],{},[2440,9624,9625],{"align":2426},"Google Calendar \u002F Sheets",[2440,9627,9628],{"align":2426},"Записи, заявки",[2440,9630,9631],{"align":2426},"Свободные слоты",[11,9633,9634,9635,60],{},"Если интеграция внутренняя (своя CRM, 1С) — приложите описание API или контакт того, кто им владеет. Как устроена связка бота с CRM технически — в ",[1270,9636,9637],{"href":8831},"отдельном руководстве с кодом",[26,9639,9641],{"id":9640},"_5-роли-и-права","5. Роли и права",[1292,9643,9644,9651,9658],{},[1232,9645,9646,9647,9650],{},"Кто ",[15,9648,9649],{},"администрирует"," бота (меняет тексты, смотрит заявки)?",[1232,9652,9653,9654,9657],{},"Кому приходят ",[15,9655,9656],{},"уведомления"," о новых заявках и оплатах?",[1232,9659,9660,9661,9664],{},"Нужна ли ",[15,9662,9663],{},"передача диалога человеку",", и кто этот человек в рабочее\u002Fнерабочее время?",[26,9666,9668],{"id":9667},"_6-нефункциональные-требования","6. Нефункциональные требования",[11,9670,9671],{},"Коротко, но явно:",[1292,9673,9674,9679,9685,9691],{},[1232,9675,9676,9678],{},[15,9677,9123],{},": сколько пользователей в день ожидаете (10? 1 000? 10 000?) — от этого зависит архитектура.",[1232,9680,9681,9684],{},[15,9682,9683],{},"Языки",": один или несколько.",[1232,9686,9687,9690],{},[15,9688,9689],{},"Данные",": где хранить персональные данные клиентов, нужен ли экспорт.",[1232,9692,9693,9696,9697,9700],{},[15,9694,9695],{},"Ограничения платформы",": у ",[1270,9698,8862],{"href":8860,"rel":9699},[1300]," есть лимиты на частоту сообщений — массовые рассылки проектируются с их учётом.",[26,9702,9704],{"id":9703},"что-происходит-с-этим-брифом-дальше","Что происходит с этим брифом дальше",[11,9706,9707,9708,9711],{},"Заполненный чек-лист — это 1–2 страницы текста. По нему я готовлю спецификацию с этапами и фиксированной сметой (ориентиры по ценам — в статье ",[1270,9709,9710],{"href":9136},"сколько стоит Telegram-бот","), мы согласовываем её, и разработка идёт с показом результатов каждые 3–5 дней.",[11,9713,9714,9715,9717,9718,60],{},"Готовы описать задачу? Отправьте бриф через страницу ",[1270,9716,9169],{"href":1319}," — отвечу с оценкой в течение 24 часов. Если сценарии пока не складываются — разберём их вместе на ",[1270,9719,9720],{"href":1324},"консультации",{"title":40,"searchDepth":103,"depth":103,"links":9722},[9723,9724,9725,9726,9727,9728,9729],{"id":9492,"depth":103,"text":9493},{"id":9516,"depth":103,"text":9517},{"id":9532,"depth":103,"text":9533},{"id":9577,"depth":103,"text":9578},{"id":9640,"depth":103,"text":9641},{"id":9667,"depth":103,"text":9668},{"id":9703,"depth":103,"text":9704},"2026-05-24","Чек-лист технического задания на Telegram-бота — сценарии, состояния, ошибки, интеграции и нефункциональные требования. Сокращает смету и сроки разработки.",[9733,9736,9739,9742],{"q":9734,"a":9735},"Обязательно ли писать формальное ТЗ, или можно своими словами?","Своими словами — нормально. Хорошее описание задачи — это сценарии диалогов и список вопросов клиентов, а не ГОСТ. Формализацию в спецификацию я беру на себя на этапе брифа.",{"q":9737,"a":9738},"Кто пишет финальное ТЗ — заказчик или разработчик?","Разработчик, на основе вашего брифа. Вы описываете бизнес-задачу и сценарии, я превращаю их в спецификацию с состояниями, интеграциями и планом этапов — и мы согласовываем её до старта.",{"q":9740,"a":9741},"Насколько подробным должно быть описание?","Достаточно 1–2 страниц: цель бота, 3–5 сценариев диалога по шагам, список интеграций и примеры реальных вопросов клиентов. Детализация сверх этого редко ускоряет проект.",{"q":9743,"a":9744},"Что делать, если требования изменятся в процессе?","Это нормально и ожидаемо. Я показываю промежуточные результаты каждые 3–5 дней, поэтому правки вносятся по ходу, а крупные изменения объёма оформляются отдельным этапом, чтобы смета оставалась прозрачной.","\u002Fimages\u002Fblog\u002Fblog_bot_spec_checklist.jpg",{},"\u002Fblog\u002Fru\u002Ftelegram-bot-specification-checklist",{"title":9484,"description":9731},"blog\u002Fru\u002Ftelegram-bot-specification-checklist",[1362,9203,9751,9752],"ТЗ","Процесс","T_yvhsHC5fQ2JD35lAQAZYq2EEhPLgmhf5JryyE4esc",{"id":9755,"title":9756,"body":9757,"date":11140,"description":11141,"extension":1344,"faq":11142,"image":11152,"lang":1356,"meta":11153,"navigation":106,"path":11154,"published":106,"readTime":213,"seo":11155,"stem":11156,"tags":11157,"updated":1342,"__hash__":11160},"blog\u002Fblog\u002Fru\u002Ftelegram-bot-crm-integration.md","Почему вашей CRM нужен кастомный Telegram-бот (amoCRM & Bitrix24)",{"type":8,"value":9758,"toc":11133},[9759,9762,9765,9768,9771,9775,9781,9785,9788,10231,10235,10238,10924,10928,10931,11083,11085,11109,11112,11122,11130],[11,9760,9761],{},"Для большинства современных компаний мессенджеры стали основным каналом коммуникации с клиентами. Однако ручная обработка входящих запросов в чатах несет в себе массу рисков: менеджеры забывают отвечать на сообщения, контакты не сохраняются в базу данных, а первичная квалификация клиента занимает часы ручной переписки.",[11,9763,9764],{},"Интеграция кастомного Telegram-бота с вашей CRM-системой (amoCRM, Bitrix24, HubSpot) позволяет полностью автоматизировать первичный контакт с клиентом.",[11,9766,9767],{},"Бот вовлекает потенциального клиента в диалог, задает ему серию квалификационных вопросов с помощью инлайн-кнопок, оценивает лид по скоринговому алгоритму и моментально создает сделку в нужном этапе воронки продаж.",[11,9769,9770],{},"В этой статье мы спроектируем воронку квалификации, напишем алгоритм скоринга на TypeScript и настроим отправку данных в amoCRM API.",[26,9772,9774],{"id":9773},"архитектура-квалификации-лидов-в-telegram","Архитектура квалификации лидов в Telegram",[34,9776,9779],{"className":9777,"code":9778,"language":39,"meta":40},[37],"[Пользователь запускает бота]\n             │\n             v\n[Вопрос 1: Бюджет проекта?] ──> [Инлайн-кнопки: до $2к, до $10к, от $10к+]\n             │\n             v\n[Вопрос 2: Направление?] ──> [Инлайн-кнопки: SaaS, Телеграм-бот, Консалтинг]\n             │\n             v\n[Запрос контактов] ──> [Кнопка отправки телефона или текстовый ввод почты]\n             │\n             v\n(Алгоритм скоринга лида)\nЕСЛИ бюджет = \"от $10к\" ТОГДА Приоритет = \"ВЫСОКИЙ\" ИНАЧЕ Приоритет = \"СРЕДНИЙ\"\n             │\n             v\n(Запрос к API amoCRM)\nСоздание Контакта ──> Создание Сделки ──> Привязка Контакта к Сделке\n             │\n             v\n[Оповещение менеджеров в Slack\u002FTelegram со ссылкой на карточку в CRM]\n",[42,9780,9778],{"__ignoreMap":40},[26,9782,9784],{"id":9783},"_1-проектирование-скоринга-лидов","1. Проектирование скоринга лидов",[11,9786,9787],{},"Прежде чем отправлять данные в CRM, мы рассчитаем балл лида на бэкенде. Это поможет вашему отделу продаж в первую очередь обрабатывать самые прибыльные сделки.",[34,9789,9791],{"className":73,"code":9790,"language":75,"meta":40,"style":40},"export interface QualificationAnswers {\n  budget: string; \u002F\u002F 'under_2k' | '2k_10k' | 'over_10k'\n  timeline: string; \u002F\u002F 'immediate' | '1_month' | 'planning'\n  projectType: string; \u002F\u002F 'saas' | 'bot' | 'consultation'\n}\n\nexport interface LeadScore {\n  score: number;\n  priority: 'LOW' | 'MEDIUM' | 'HIGH';\n}\n\nexport function calculateLeadScore(answers: QualificationAnswers): LeadScore {\n  let score = 0;\n\n  \u002F\u002F 1. Оценка бюджета\n  if (answers.budget === 'over_10k') score += 50;\n  else if (answers.budget === '2k_10k') score += 30;\n  else score += 10;\n\n  \u002F\u002F 2. Оценка срочности запуска\n  if (answers.timeline === 'immediate') score += 30;\n  else if (answers.timeline === '1_month') score += 20;\n  else score += 5;\n\n  \u002F\u002F 3. Оценка соответствия стеку услуг\n  if (answers.projectType === 'saas') score += 20;\n  else if (answers.projectType === 'bot') score += 15;\n  else score += 5;\n\n  let priority: 'LOW' | 'MEDIUM' | 'HIGH' = 'LOW';\n  if (score >= 70) priority = 'HIGH';\n  else if (score >= 40) priority = 'MEDIUM';\n\n  return { score, priority };\n}\n",[42,9792,9793,9804,9819,9833,9847,9851,9855,9866,9877,9900,9904,9908,9934,9948,9952,9957,9980,10004,10017,10021,10026,10046,10068,10081,10085,10090,10110,10132,10144,10148,10173,10195,10216,10220,10227],{"__ignoreMap":40},[79,9794,9795,9797,9799,9802],{"class":81,"line":82},[79,9796,260],{"class":85},[79,9798,4086],{"class":85},[79,9800,9801],{"class":116}," QualificationAnswers",[79,9803,120],{"class":89},[79,9805,9806,9809,9811,9813,9816],{"class":81,"line":103},[79,9807,9808],{"class":126},"  budget",[79,9810,130],{"class":85},[79,9812,147],{"class":133},[79,9814,9815],{"class":89},"; ",[79,9817,9818],{"class":393},"\u002F\u002F 'under_2k' | '2k_10k' | 'over_10k'\n",[79,9820,9821,9824,9826,9828,9830],{"class":81,"line":110},[79,9822,9823],{"class":126},"  timeline",[79,9825,130],{"class":85},[79,9827,147],{"class":133},[79,9829,9815],{"class":89},[79,9831,9832],{"class":393},"\u002F\u002F 'immediate' | '1_month' | 'planning'\n",[79,9834,9835,9838,9840,9842,9844],{"class":81,"line":123},[79,9836,9837],{"class":126},"  projectType",[79,9839,130],{"class":85},[79,9841,147],{"class":133},[79,9843,9815],{"class":89},[79,9845,9846],{"class":393},"\u002F\u002F 'saas' | 'bot' | 'consultation'\n",[79,9848,9849],{"class":81,"line":139},[79,9850,205],{"class":89},[79,9852,9853],{"class":81,"line":152},[79,9854,107],{"emptyLinePlaceholder":106},[79,9856,9857,9859,9861,9864],{"class":81,"line":165},[79,9858,260],{"class":85},[79,9860,4086],{"class":85},[79,9862,9863],{"class":116}," LeadScore",[79,9865,120],{"class":89},[79,9867,9868,9871,9873,9875],{"class":81,"line":177},[79,9869,9870],{"class":126},"  score",[79,9872,130],{"class":85},[79,9874,134],{"class":133},[79,9876,100],{"class":89},[79,9878,9879,9882,9884,9887,9890,9893,9895,9898],{"class":81,"line":189},[79,9880,9881],{"class":126},"  priority",[79,9883,130],{"class":85},[79,9885,9886],{"class":96}," 'LOW'",[79,9888,9889],{"class":85}," |",[79,9891,9892],{"class":96}," 'MEDIUM'",[79,9894,9889],{"class":85},[79,9896,9897],{"class":96}," 'HIGH'",[79,9899,100],{"class":89},[79,9901,9902],{"class":81,"line":202},[79,9903,205],{"class":89},[79,9905,9906],{"class":81,"line":208},[79,9907,107],{"emptyLinePlaceholder":106},[79,9909,9910,9912,9914,9917,9919,9922,9924,9926,9928,9930,9932],{"class":81,"line":213},[79,9911,260],{"class":85},[79,9913,263],{"class":85},[79,9915,9916],{"class":116}," calculateLeadScore",[79,9918,269],{"class":89},[79,9920,9921],{"class":126},"answers",[79,9923,130],{"class":85},[79,9925,9801],{"class":116},[79,9927,289],{"class":89},[79,9929,130],{"class":85},[79,9931,9863],{"class":116},[79,9933,120],{"class":89},[79,9935,9936,9939,9942,9944,9946],{"class":81,"line":223},[79,9937,9938],{"class":85},"  let",[79,9940,9941],{"class":89}," score ",[79,9943,1090],{"class":85},[79,9945,8028],{"class":133},[79,9947,100],{"class":89},[79,9949,9950],{"class":81,"line":235},[79,9951,107],{"emptyLinePlaceholder":106},[79,9953,9954],{"class":81,"line":247},[79,9955,9956],{"class":393},"  \u002F\u002F 1. Оценка бюджета\n",[79,9958,9959,9961,9964,9966,9969,9972,9975,9978],{"class":81,"line":252},[79,9960,352],{"class":85},[79,9962,9963],{"class":89}," (answers.budget ",[79,9965,692],{"class":85},[79,9967,9968],{"class":96}," 'over_10k'",[79,9970,9971],{"class":89},") score ",[79,9973,9974],{"class":85},"+=",[79,9976,9977],{"class":133}," 50",[79,9979,100],{"class":89},[79,9981,9982,9985,9988,9990,9992,9995,9997,9999,10002],{"class":81,"line":257},[79,9983,9984],{"class":85},"  else",[79,9986,9987],{"class":85}," if",[79,9989,9963],{"class":89},[79,9991,692],{"class":85},[79,9993,9994],{"class":96}," '2k_10k'",[79,9996,9971],{"class":89},[79,9998,9974],{"class":85},[79,10000,10001],{"class":133}," 30",[79,10003,100],{"class":89},[79,10005,10006,10008,10010,10012,10015],{"class":81,"line":298},[79,10007,9984],{"class":85},[79,10009,9941],{"class":89},[79,10011,9974],{"class":85},[79,10013,10014],{"class":133}," 10",[79,10016,100],{"class":89},[79,10018,10019],{"class":81,"line":319},[79,10020,107],{"emptyLinePlaceholder":106},[79,10022,10023],{"class":81,"line":343},[79,10024,10025],{"class":393},"  \u002F\u002F 2. Оценка срочности запуска\n",[79,10027,10028,10030,10033,10035,10038,10040,10042,10044],{"class":81,"line":349},[79,10029,352],{"class":85},[79,10031,10032],{"class":89}," (answers.timeline ",[79,10034,692],{"class":85},[79,10036,10037],{"class":96}," 'immediate'",[79,10039,9971],{"class":89},[79,10041,9974],{"class":85},[79,10043,10001],{"class":133},[79,10045,100],{"class":89},[79,10047,10048,10050,10052,10054,10056,10059,10061,10063,10066],{"class":81,"line":364},[79,10049,9984],{"class":85},[79,10051,9987],{"class":85},[79,10053,10032],{"class":89},[79,10055,692],{"class":85},[79,10057,10058],{"class":96}," '1_month'",[79,10060,9971],{"class":89},[79,10062,9974],{"class":85},[79,10064,10065],{"class":133}," 20",[79,10067,100],{"class":89},[79,10069,10070,10072,10074,10076,10079],{"class":81,"line":379},[79,10071,9984],{"class":85},[79,10073,9941],{"class":89},[79,10075,9974],{"class":85},[79,10077,10078],{"class":133}," 5",[79,10080,100],{"class":89},[79,10082,10083],{"class":81,"line":385},[79,10084,107],{"emptyLinePlaceholder":106},[79,10086,10087],{"class":81,"line":390},[79,10088,10089],{"class":393},"  \u002F\u002F 3. Оценка соответствия стеку услуг\n",[79,10091,10092,10094,10097,10099,10102,10104,10106,10108],{"class":81,"line":397},[79,10093,352],{"class":85},[79,10095,10096],{"class":89}," (answers.projectType ",[79,10098,692],{"class":85},[79,10100,10101],{"class":96}," 'saas'",[79,10103,9971],{"class":89},[79,10105,9974],{"class":85},[79,10107,10065],{"class":133},[79,10109,100],{"class":89},[79,10111,10112,10114,10116,10118,10120,10123,10125,10127,10130],{"class":81,"line":450},[79,10113,9984],{"class":85},[79,10115,9987],{"class":85},[79,10117,10096],{"class":89},[79,10119,692],{"class":85},[79,10121,10122],{"class":96}," 'bot'",[79,10124,9971],{"class":89},[79,10126,9974],{"class":85},[79,10128,10129],{"class":133}," 15",[79,10131,100],{"class":89},[79,10133,10134,10136,10138,10140,10142],{"class":81,"line":455},[79,10135,9984],{"class":85},[79,10137,9941],{"class":89},[79,10139,9974],{"class":85},[79,10141,10078],{"class":133},[79,10143,100],{"class":89},[79,10145,10146],{"class":81,"line":461},[79,10147,107],{"emptyLinePlaceholder":106},[79,10149,10150,10152,10155,10157,10159,10161,10163,10165,10167,10169,10171],{"class":81,"line":474},[79,10151,9938],{"class":85},[79,10153,10154],{"class":89}," priority",[79,10156,130],{"class":85},[79,10158,9886],{"class":96},[79,10160,9889],{"class":85},[79,10162,9892],{"class":96},[79,10164,9889],{"class":85},[79,10166,9897],{"class":96},[79,10168,307],{"class":85},[79,10170,9886],{"class":96},[79,10172,100],{"class":89},[79,10174,10175,10177,10180,10183,10186,10189,10191,10193],{"class":81,"line":516},[79,10176,352],{"class":85},[79,10178,10179],{"class":89}," (score ",[79,10181,10182],{"class":85},">=",[79,10184,10185],{"class":133}," 70",[79,10187,10188],{"class":89},") priority ",[79,10190,1090],{"class":85},[79,10192,9897],{"class":96},[79,10194,100],{"class":89},[79,10196,10197,10199,10201,10203,10205,10208,10210,10212,10214],{"class":81,"line":536},[79,10198,9984],{"class":85},[79,10200,9987],{"class":85},[79,10202,10179],{"class":89},[79,10204,10182],{"class":85},[79,10206,10207],{"class":133}," 40",[79,10209,10188],{"class":89},[79,10211,1090],{"class":85},[79,10213,9892],{"class":96},[79,10215,100],{"class":89},[79,10217,10218],{"class":81,"line":541},[79,10219,107],{"emptyLinePlaceholder":106},[79,10221,10222,10224],{"class":81,"line":547},[79,10223,1204],{"class":85},[79,10225,10226],{"class":89}," { score, priority };\n",[79,10228,10229],{"class":81,"line":553},[79,10230,205],{"class":89},[26,10232,10234],{"id":10233},"_2-передача-сделок-в-api-amocrm","2. Передача сделок в API amoCRM",[11,10236,10237],{},"Пример реализации на Node.js для создания контакта и сделки в amoCRM с использованием библиотеки Axios.",[34,10239,10241],{"className":73,"code":10240,"language":75,"meta":40,"style":40},"import axios from 'axios';\n\ninterface LeadPayload {\n  firstName: string;\n  phone: string;\n  telegramUsername?: string;\n  score: LeadScore;\n  answers: QualificationAnswers;\n}\n\nexport async function pushLeadToAmoCRM(lead: LeadPayload) {\n  const subdomain = process.env.AMOCRM_SUBDOMAIN;\n  const token = process.env.AMOCRM_ACCESS_TOKEN;\n  \n  const headers = {\n    Authorization: `Bearer ${token}`,\n    'Content-Type': 'application\u002Fjson',\n  };\n\n  try {\n    \u002F\u002F 1. Создаем контакт в amoCRM\n    const contactRes = await axios.post(\n      `https:\u002F\u002F${subdomain}.amocrm.ru\u002Fapi\u002Fv4\u002Fcontacts`,\n      [{\n        name: lead.firstName,\n        custom_fields_values: [\n          {\n            field_id: 111111, \u002F\u002F ID поля \"Телефон\" в вашей amoCRM\n            values: [{ value: lead.phone, enum_code: 'MOB' }]\n          },\n          {\n            field_id: 222222, \u002F\u002F ID поля \"Telegram\" в вашей amoCRM\n            values: [{ value: lead.telegramUsername ? `@${lead.telegramUsername}` : 'N\u002FA' }]\n          }\n        ]\n      }],\n      { headers }\n    );\n\n    const contactId = contactRes.data._embedded.contacts[0].id;\n\n    \u002F\u002F 2. Создаем сделку с указанием рассчитанной приоритетности\n    const leadName = `Лид [${lead.score.priority}]: ${lead.firstName}`;\n    const budgetValue = lead.answers.budget === 'over_10k' ? 10000 : (lead.answers.budget === '2k_10k' ? 5000 : 1500);\n\n    const dealRes = await axios.post(\n      `https:\u002F\u002F${subdomain}.amocrm.ru\u002Fapi\u002Fv4\u002Fleads`,\n      [{\n        name: leadName,\n        price: budgetValue,\n        _embedded: {\n          contacts: [{ id: contactId }]\n        },\n        custom_fields_values: [\n          {\n            field_id: 333333, \u002F\u002F ID кастомного поля \"Тип проекта\"\n            values: [{ value: lead.answers.projectType }]\n          },\n          {\n            field_id: 444444, \u002F\u002F ID кастомного поля \"Оценка ИИ\"\n            values: [{ value: `Балл: ${lead.score.score}\u002F100` }]\n          }\n        ]\n      }],\n      { headers }\n    );\n\n    return dealRes.data;\n  } catch (error: any) {\n    console.error('[CRM Error] amoCRM API Sync failed:', error.response?.data || error.message);\n    throw new Error('CRM Sync Error');\n  }\n}\n",[42,10242,10243,10257,10261,10270,10281,10292,10303,10313,10324,10328,10332,10354,10370,10386,10390,10401,10416,10427,10432,10436,10442,10447,10465,10478,10483,10488,10493,10498,10511,10522,10527,10531,10543,10570,10575,10580,10585,10590,10595,10599,10616,10620,10625,10663,10706,10710,10727,10738,10742,10747,10752,10757,10762,10767,10771,10775,10787,10792,10796,10800,10812,10835,10839,10843,10847,10851,10855,10859,10866,10882,10901,10916,10920],{"__ignoreMap":40},[79,10244,10245,10247,10250,10252,10255],{"class":81,"line":82},[79,10246,86],{"class":85},[79,10248,10249],{"class":89}," axios ",[79,10251,93],{"class":85},[79,10253,10254],{"class":96}," 'axios'",[79,10256,100],{"class":89},[79,10258,10259],{"class":81,"line":103},[79,10260,107],{"emptyLinePlaceholder":106},[79,10262,10263,10265,10268],{"class":81,"line":110},[79,10264,113],{"class":85},[79,10266,10267],{"class":116}," LeadPayload",[79,10269,120],{"class":89},[79,10271,10272,10275,10277,10279],{"class":81,"line":123},[79,10273,10274],{"class":126},"  firstName",[79,10276,130],{"class":85},[79,10278,147],{"class":133},[79,10280,100],{"class":89},[79,10282,10283,10286,10288,10290],{"class":81,"line":139},[79,10284,10285],{"class":126},"  phone",[79,10287,130],{"class":85},[79,10289,147],{"class":133},[79,10291,100],{"class":89},[79,10293,10294,10297,10299,10301],{"class":81,"line":152},[79,10295,10296],{"class":126},"  telegramUsername",[79,10298,158],{"class":85},[79,10300,147],{"class":133},[79,10302,100],{"class":89},[79,10304,10305,10307,10309,10311],{"class":81,"line":165},[79,10306,9870],{"class":126},[79,10308,130],{"class":85},[79,10310,9863],{"class":116},[79,10312,100],{"class":89},[79,10314,10315,10318,10320,10322],{"class":81,"line":177},[79,10316,10317],{"class":126},"  answers",[79,10319,130],{"class":85},[79,10321,9801],{"class":116},[79,10323,100],{"class":89},[79,10325,10326],{"class":81,"line":189},[79,10327,205],{"class":89},[79,10329,10330],{"class":81,"line":202},[79,10331,107],{"emptyLinePlaceholder":106},[79,10333,10334,10336,10338,10340,10343,10345,10348,10350,10352],{"class":81,"line":208},[79,10335,260],{"class":85},[79,10337,4139],{"class":85},[79,10339,263],{"class":85},[79,10341,10342],{"class":116}," pushLeadToAmoCRM",[79,10344,269],{"class":89},[79,10346,10347],{"class":126},"lead",[79,10349,130],{"class":85},[79,10351,10267],{"class":116},[79,10353,5161],{"class":89},[79,10355,10356,10358,10361,10363,10365,10368],{"class":81,"line":213},[79,10357,301],{"class":85},[79,10359,10360],{"class":133}," subdomain",[79,10362,307],{"class":85},[79,10364,4925],{"class":89},[79,10366,10367],{"class":133},"AMOCRM_SUBDOMAIN",[79,10369,100],{"class":89},[79,10371,10372,10374,10377,10379,10381,10384],{"class":81,"line":223},[79,10373,301],{"class":85},[79,10375,10376],{"class":133}," token",[79,10378,307],{"class":85},[79,10380,4925],{"class":89},[79,10382,10383],{"class":133},"AMOCRM_ACCESS_TOKEN",[79,10385,100],{"class":89},[79,10387,10388],{"class":81,"line":235},[79,10389,346],{"class":89},[79,10391,10392,10394,10397,10399],{"class":81,"line":247},[79,10393,301],{"class":85},[79,10395,10396],{"class":133}," headers",[79,10398,307],{"class":85},[79,10400,120],{"class":89},[79,10402,10403,10406,10409,10412,10414],{"class":81,"line":252},[79,10404,10405],{"class":89},"    Authorization: ",[79,10407,10408],{"class":96},"`Bearer ${",[79,10410,10411],{"class":89},"token",[79,10413,510],{"class":96},[79,10415,2604],{"class":89},[79,10417,10418,10421,10423,10425],{"class":81,"line":257},[79,10419,10420],{"class":96},"    'Content-Type'",[79,10422,2598],{"class":89},[79,10424,4978],{"class":96},[79,10426,2604],{"class":89},[79,10428,10429],{"class":81,"line":298},[79,10430,10431],{"class":89},"  };\n",[79,10433,10434],{"class":81,"line":319},[79,10435,107],{"emptyLinePlaceholder":106},[79,10437,10438,10440],{"class":81,"line":343},[79,10439,745],{"class":85},[79,10441,120],{"class":89},[79,10443,10444],{"class":81,"line":349},[79,10445,10446],{"class":393},"    \u002F\u002F 1. Создаем контакт в amoCRM\n",[79,10448,10449,10451,10454,10456,10458,10461,10463],{"class":81,"line":364},[79,10450,753],{"class":85},[79,10452,10453],{"class":133}," contactRes",[79,10455,307],{"class":85},[79,10457,4180],{"class":85},[79,10459,10460],{"class":89}," axios.",[79,10462,4954],{"class":116},[79,10464,4957],{"class":89},[79,10466,10467,10470,10473,10476],{"class":81,"line":379},[79,10468,10469],{"class":96},"      `https:\u002F\u002F${",[79,10471,10472],{"class":89},"subdomain",[79,10474,10475],{"class":96},"}.amocrm.ru\u002Fapi\u002Fv4\u002Fcontacts`",[79,10477,2604],{"class":89},[79,10479,10480],{"class":81,"line":385},[79,10481,10482],{"class":89},"      [{\n",[79,10484,10485],{"class":81,"line":390},[79,10486,10487],{"class":89},"        name: lead.firstName,\n",[79,10489,10490],{"class":81,"line":397},[79,10491,10492],{"class":89},"        custom_fields_values: [\n",[79,10494,10495],{"class":81,"line":450},[79,10496,10497],{"class":89},"          {\n",[79,10499,10500,10503,10506,10508],{"class":81,"line":455},[79,10501,10502],{"class":89},"            field_id: ",[79,10504,10505],{"class":133},"111111",[79,10507,279],{"class":89},[79,10509,10510],{"class":393},"\u002F\u002F ID поля \"Телефон\" в вашей amoCRM\n",[79,10512,10513,10516,10519],{"class":81,"line":461},[79,10514,10515],{"class":89},"            values: [{ value: lead.phone, enum_code: ",[79,10517,10518],{"class":96},"'MOB'",[79,10520,10521],{"class":89}," }]\n",[79,10523,10524],{"class":81,"line":474},[79,10525,10526],{"class":89},"          },\n",[79,10528,10529],{"class":81,"line":516},[79,10530,10497],{"class":89},[79,10532,10533,10535,10538,10540],{"class":81,"line":536},[79,10534,10502],{"class":89},[79,10536,10537],{"class":133},"222222",[79,10539,279],{"class":89},[79,10541,10542],{"class":393},"\u002F\u002F ID поля \"Telegram\" в вашей amoCRM\n",[79,10544,10545,10548,10550,10553,10555,10557,10560,10562,10565,10568],{"class":81,"line":541},[79,10546,10547],{"class":89},"            values: [{ value: lead.telegramUsername ",[79,10549,789],{"class":85},[79,10551,10552],{"class":96}," `@${",[79,10554,10347],{"class":89},[79,10556,60],{"class":96},[79,10558,10559],{"class":89},"telegramUsername",[79,10561,510],{"class":96},[79,10563,10564],{"class":85}," :",[79,10566,10567],{"class":96}," 'N\u002FA'",[79,10569,10521],{"class":89},[79,10571,10572],{"class":81,"line":547},[79,10573,10574],{"class":89},"          }\n",[79,10576,10577],{"class":81,"line":553},[79,10578,10579],{"class":89},"        ]\n",[79,10581,10582],{"class":81,"line":566},[79,10583,10584],{"class":89},"      }],\n",[79,10586,10587],{"class":81,"line":586},[79,10588,10589],{"class":89},"      { headers }\n",[79,10591,10592],{"class":81,"line":597},[79,10593,10594],{"class":89},"    );\n",[79,10596,10597],{"class":81,"line":607},[79,10598,107],{"emptyLinePlaceholder":106},[79,10600,10601,10603,10606,10608,10611,10613],{"class":81,"line":612},[79,10602,753],{"class":85},[79,10604,10605],{"class":133}," contactId",[79,10607,307],{"class":85},[79,10609,10610],{"class":89}," contactRes.data._embedded.contacts[",[79,10612,5652],{"class":133},[79,10614,10615],{"class":89},"].id;\n",[79,10617,10618],{"class":81,"line":618},[79,10619,107],{"emptyLinePlaceholder":106},[79,10621,10622],{"class":81,"line":630},[79,10623,10624],{"class":393},"    \u002F\u002F 2. Создаем сделку с указанием рассчитанной приоритетности\n",[79,10626,10627,10629,10632,10634,10637,10639,10641,10644,10646,10649,10652,10654,10656,10659,10661],{"class":81,"line":644},[79,10628,753],{"class":85},[79,10630,10631],{"class":133}," leadName",[79,10633,307],{"class":85},[79,10635,10636],{"class":96}," `Лид [${",[79,10638,10347],{"class":89},[79,10640,60],{"class":96},[79,10642,10643],{"class":89},"score",[79,10645,60],{"class":96},[79,10647,10648],{"class":89},"priority",[79,10650,10651],{"class":96},"}]: ${",[79,10653,10347],{"class":89},[79,10655,60],{"class":96},[79,10657,10658],{"class":89},"firstName",[79,10660,510],{"class":96},[79,10662,100],{"class":89},[79,10664,10665,10667,10670,10672,10675,10677,10679,10682,10685,10687,10690,10692,10694,10696,10699,10701,10704],{"class":81,"line":654},[79,10666,753],{"class":85},[79,10668,10669],{"class":133}," budgetValue",[79,10671,307],{"class":85},[79,10673,10674],{"class":89}," lead.answers.budget ",[79,10676,692],{"class":85},[79,10678,9968],{"class":96},[79,10680,10681],{"class":85}," ?",[79,10683,10684],{"class":133}," 10000",[79,10686,10564],{"class":85},[79,10688,10689],{"class":89}," (lead.answers.budget ",[79,10691,692],{"class":85},[79,10693,9994],{"class":96},[79,10695,10681],{"class":85},[79,10697,10698],{"class":133}," 5000",[79,10700,10564],{"class":85},[79,10702,10703],{"class":133}," 1500",[79,10705,340],{"class":89},[79,10707,10708],{"class":81,"line":668},[79,10709,107],{"emptyLinePlaceholder":106},[79,10711,10712,10714,10717,10719,10721,10723,10725],{"class":81,"line":673},[79,10713,753],{"class":85},[79,10715,10716],{"class":133}," dealRes",[79,10718,307],{"class":85},[79,10720,4180],{"class":85},[79,10722,10460],{"class":89},[79,10724,4954],{"class":116},[79,10726,4957],{"class":89},[79,10728,10729,10731,10733,10736],{"class":81,"line":679},[79,10730,10469],{"class":96},[79,10732,10472],{"class":89},[79,10734,10735],{"class":96},"}.amocrm.ru\u002Fapi\u002Fv4\u002Fleads`",[79,10737,2604],{"class":89},[79,10739,10740],{"class":81,"line":698},[79,10741,10482],{"class":89},[79,10743,10744],{"class":81,"line":703},[79,10745,10746],{"class":89},"        name: leadName,\n",[79,10748,10749],{"class":81,"line":715},[79,10750,10751],{"class":89},"        price: budgetValue,\n",[79,10753,10754],{"class":81,"line":726},[79,10755,10756],{"class":89},"        _embedded: {\n",[79,10758,10759],{"class":81,"line":731},[79,10760,10761],{"class":89},"          contacts: [{ id: contactId }]\n",[79,10763,10764],{"class":81,"line":736},[79,10765,10766],{"class":89},"        },\n",[79,10768,10769],{"class":81,"line":742},[79,10770,10492],{"class":89},[79,10772,10773],{"class":81,"line":750},[79,10774,10497],{"class":89},[79,10776,10777,10779,10782,10784],{"class":81,"line":772},[79,10778,10502],{"class":89},[79,10780,10781],{"class":133},"333333",[79,10783,279],{"class":89},[79,10785,10786],{"class":393},"\u002F\u002F ID кастомного поля \"Тип проекта\"\n",[79,10788,10789],{"class":81,"line":810},[79,10790,10791],{"class":89},"            values: [{ value: lead.answers.projectType }]\n",[79,10793,10794],{"class":81,"line":823},[79,10795,10526],{"class":89},[79,10797,10798],{"class":81,"line":835},[79,10799,10497],{"class":89},[79,10801,10802,10804,10807,10809],{"class":81,"line":846},[79,10803,10502],{"class":89},[79,10805,10806],{"class":133},"444444",[79,10808,279],{"class":89},[79,10810,10811],{"class":393},"\u002F\u002F ID кастомного поля \"Оценка ИИ\"\n",[79,10813,10814,10817,10820,10822,10824,10826,10828,10830,10833],{"class":81,"line":851},[79,10815,10816],{"class":89},"            values: [{ value: ",[79,10818,10819],{"class":96},"`Балл: ${",[79,10821,10347],{"class":89},[79,10823,60],{"class":96},[79,10825,10643],{"class":89},[79,10827,60],{"class":96},[79,10829,10643],{"class":89},[79,10831,10832],{"class":96},"}\u002F100`",[79,10834,10521],{"class":89},[79,10836,10837],{"class":81,"line":5532},[79,10838,10574],{"class":89},[79,10840,10841],{"class":81,"line":5537},[79,10842,10579],{"class":89},[79,10844,10845],{"class":81,"line":5562},[79,10846,10584],{"class":89},[79,10848,10849],{"class":81,"line":5575},[79,10850,10589],{"class":89},[79,10852,10853],{"class":81,"line":5587},[79,10854,10594],{"class":89},[79,10856,10857],{"class":81,"line":5613},[79,10858,107],{"emptyLinePlaceholder":106},[79,10860,10861,10863],{"class":81,"line":5618},[79,10862,367],{"class":85},[79,10864,10865],{"class":89}," dealRes.data;\n",[79,10867,10868,10870,10872,10874,10876,10878,10880],{"class":81,"line":5623},[79,10869,826],{"class":89},[79,10871,829],{"class":85},[79,10873,355],{"class":89},[79,10875,5169],{"class":126},[79,10877,130],{"class":85},[79,10879,1043],{"class":133},[79,10881,5161],{"class":89},[79,10883,10884,10886,10888,10890,10893,10896,10898],{"class":81,"line":5639},[79,10885,8771],{"class":89},[79,10887,5169],{"class":116},[79,10889,269],{"class":89},[79,10891,10892],{"class":96},"'[CRM Error] amoCRM API Sync failed:'",[79,10894,10895],{"class":89},", error.response?.data ",[79,10897,8361],{"class":85},[79,10899,10900],{"class":89}," error.message);\n",[79,10902,10903,10905,10907,10909,10911,10914],{"class":81,"line":5658},[79,10904,4333],{"class":85},[79,10906,310],{"class":85},[79,10908,4338],{"class":116},[79,10910,269],{"class":89},[79,10912,10913],{"class":96},"'CRM Sync Error'",[79,10915,340],{"class":89},[79,10917,10918],{"class":81,"line":5684},[79,10919,382],{"class":89},[79,10921,10922],{"class":81,"line":5689},[79,10923,205],{"class":89},[26,10925,10927],{"id":10926},"_3-оптимизация-uiux-внутри-telegram","3. Оптимизация UI\u002FUX внутри Telegram",[11,10929,10930],{},"Для повышения конверсии избегайте ручного ввода текста пользователем. Используйте нативные кнопки Inline Keyboard:",[34,10932,10934],{"className":73,"code":10933,"language":75,"meta":40,"style":40},"import TelegramBot from 'node-telegram-bot-api';\n\nexport function sendBudgetQuestion(bot: TelegramBot, chatId: number) {\n  bot.sendMessage(chatId, \"Выберите примерный бюджет вашего проекта:\", {\n    reply_markup: {\n      inline_keyboard: [\n        [\n          { text: \"До $2,000\", callback_data: \"budget_under_2k\" },\n          { text: \"$2,000 - $10,000\", callback_data: \"budget_2k_10k\" }\n        ],\n        [\n          { text: \"Более $10,000\", callback_data: \"budget_over_10k\" }\n        ]\n      ]\n    }\n  });\n}\n",[42,10935,10936,10948,10952,10981,10995,11000,11005,11010,11026,11040,11045,11049,11063,11067,11071,11075,11079],{"__ignoreMap":40},[79,10937,10938,10940,10942,10944,10946],{"class":81,"line":82},[79,10939,86],{"class":85},[79,10941,8392],{"class":89},[79,10943,93],{"class":85},[79,10945,8397],{"class":96},[79,10947,100],{"class":89},[79,10949,10950],{"class":81,"line":103},[79,10951,107],{"emptyLinePlaceholder":106},[79,10953,10954,10956,10958,10961,10963,10966,10968,10970,10972,10975,10977,10979],{"class":81,"line":110},[79,10955,260],{"class":85},[79,10957,263],{"class":85},[79,10959,10960],{"class":116}," sendBudgetQuestion",[79,10962,269],{"class":89},[79,10964,10965],{"class":126},"bot",[79,10967,130],{"class":85},[79,10969,8417],{"class":116},[79,10971,279],{"class":89},[79,10973,10974],{"class":126},"chatId",[79,10976,130],{"class":85},[79,10978,134],{"class":133},[79,10980,5161],{"class":89},[79,10982,10983,10986,10988,10990,10993],{"class":81,"line":123},[79,10984,10985],{"class":89},"  bot.",[79,10987,8707],{"class":116},[79,10989,8792],{"class":89},[79,10991,10992],{"class":96},"\"Выберите примерный бюджет вашего проекта:\"",[79,10994,4061],{"class":89},[79,10996,10997],{"class":81,"line":139},[79,10998,10999],{"class":89},"    reply_markup: {\n",[79,11001,11002],{"class":81,"line":152},[79,11003,11004],{"class":89},"      inline_keyboard: [\n",[79,11006,11007],{"class":81,"line":165},[79,11008,11009],{"class":89},"        [\n",[79,11011,11012,11015,11018,11021,11024],{"class":81,"line":177},[79,11013,11014],{"class":89},"          { text: ",[79,11016,11017],{"class":96},"\"До $2,000\"",[79,11019,11020],{"class":89},", callback_data: ",[79,11022,11023],{"class":96},"\"budget_under_2k\"",[79,11025,3418],{"class":89},[79,11027,11028,11030,11033,11035,11038],{"class":81,"line":189},[79,11029,11014],{"class":89},[79,11031,11032],{"class":96},"\"$2,000 - $10,000\"",[79,11034,11020],{"class":89},[79,11036,11037],{"class":96},"\"budget_2k_10k\"",[79,11039,909],{"class":89},[79,11041,11042],{"class":81,"line":202},[79,11043,11044],{"class":89},"        ],\n",[79,11046,11047],{"class":81,"line":208},[79,11048,11009],{"class":89},[79,11050,11051,11053,11056,11058,11061],{"class":81,"line":213},[79,11052,11014],{"class":89},[79,11054,11055],{"class":96},"\"Более $10,000\"",[79,11057,11020],{"class":89},[79,11059,11060],{"class":96},"\"budget_over_10k\"",[79,11062,909],{"class":89},[79,11064,11065],{"class":81,"line":223},[79,11066,10579],{"class":89},[79,11068,11069],{"class":81,"line":235},[79,11070,3139],{"class":89},[79,11072,11073],{"class":81,"line":247},[79,11074,1190],{"class":89},[79,11076,11077],{"class":81,"line":252},[79,11078,1195],{"class":89},[79,11080,11081],{"class":81,"line":257},[79,11082,205],{"class":89},[26,11084,1290],{"id":1289},[1292,11086,11087,11093,11101],{},[1232,11088,11089,11092],{},[1270,11090,8862],{"href":8860,"rel":11091},[1300]," — инлайн-кнопки, callback-запросы и приём контактов",[1232,11094,11095,11100],{},[1270,11096,11099],{"href":11097,"rel":11098},"https:\u002F\u002Fwww.amocrm.ru\u002Fdevelopers",[1300],"Документация API amoCRM"," — методы создания контактов и сделок",[1232,11102,11103,11108],{},[1270,11104,11107],{"href":11105,"rel":11106},"https:\u002F\u002Fapidocs.bitrix24.ru\u002F",[1300],"REST API Bitrix24"," — аналогичные методы для Bitrix24",[11,11110,11111],{},"Автоматизация сбора контактов и мгновенная отправка лидов в amoCRM исключает влияние человеческого фактора: менеджеры не теряют переписки, а клиент получает ответ в течение первой минуты.",[11,11113,11114,11115,11117,11118,11121],{},"В моих проектах разработки ботов, таких как конструктор ",[1270,11116,1273],{"href":1272},", мы активно применяем подобные сценарии синхронизации для автоматической отправки активности пользователей в CRM. Для построения еще более сложной воронки можно объединить CRM-систему с ",[1270,11119,11120],{"href":1277},"ИИ-ботом поддержки на базе RAG",", который будет квалифицировать лида и решать его первичные вопросы в диалоге.",[11,11123,11124,11125,1321,11128,60],{},"Если вы хотите разработать кастомного Telegram-бота с интеграцией в amoCRM или Bitrix24 под задачи вашего отдела продаж, посмотрите мою услугу ",[1270,11126,11127],{"href":1319},"Создание Telegram-ботов под ключ",[1270,11129,1325],{"href":1324},[1328,11131,11132],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}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 pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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":11134},[11135,11136,11137,11138,11139],{"id":9773,"depth":103,"text":9774},{"id":9783,"depth":103,"text":9784},{"id":10233,"depth":103,"text":10234},{"id":10926,"depth":103,"text":10927},{"id":1289,"depth":103,"text":1290},"2026-05-21","Полное техническое руководство по проектированию и разработке квалифицирующего Telegram-бота на Node.js, расчете скоринга лидов и синхронизации с API amoCRM.",[11143,11146,11149],{"q":11144,"a":11145},"Зачем боту скоринг лидов, если CRM и так собирает заявки?","Скоринг сортирует заявки до того, как их увидит менеджер: лид с бюджетом от $10 000 и срочным запуском попадает в CRM с высоким приоритетом и мгновенным уведомлением, а холодные заявки не отвлекают отдел продаж. Это ускоряет ответ самым прибыльным клиентам.",{"q":11147,"a":11148},"С какими CRM можно интегрировать Telegram-бота?","С любой системой, у которой есть API: amoCRM, Bitrix24, HubSpot, а также самописные CRM. Бот создаёт контакт, сделку в нужном этапе воронки и передаёт ответы анкеты в кастомные поля.",{"q":11150,"a":11151},"Почему в боте лучше кнопки, чем свободный ввод текста?","Инлайн-кнопки повышают конверсию прохождения анкеты: пользователю не нужно печатать, а бот получает предсказуемые данные без парсинга свободного текста. Ручной ввод остаётся только там, где без него нельзя — телефон или email.","\u002Fimages\u002Fblog\u002Fblog_crm_integration.jpg",{},"\u002Fblog\u002Fru\u002Ftelegram-bot-crm-integration",{"title":9756,"description":11141},"blog\u002Fru\u002Ftelegram-bot-crm-integration",[1362,11158,6091,1365,11159],"CRM","Автоматизация","vbzgwNeMvxM71lxL3mvf1OEvm-n5wPx-xU99morqIx4",{"id":11162,"title":11163,"body":11164,"date":11426,"description":11427,"extension":1344,"faq":11428,"image":11209,"lang":1356,"meta":11429,"navigation":106,"path":11430,"published":106,"readTime":139,"seo":11431,"stem":11432,"tags":11433,"updated":1342,"__hash__":11441},"blog\u002Fblog\u002Fru\u002Fezswitch-macos-switcher.md","EzSwitch — утилита для быстрого переключения раскладки на macOS",{"type":8,"value":11165,"toc":11414},[11166,11177,11190,11193,11197,11200,11204,11211,11222,11237,11241,11252,11260,11264,11267,11271,11282,11328,11335,11339,11346,11352,11356,11362,11389,11393,11401,11411],[11,11167,11168,11169,11172,11173,11176],{},"Для тех, кто постоянно работает с текстом на нескольких языках (например, пишет код на английском и документацию на русском), раскладка клавиатуры часто становится источником раздражения. Промахи мимо нужного языка, набранный абракадаброй текст (",[42,11170,11171],{},"ghbdtn"," вместо ",[42,11174,11175],{},"привет",") — всё это сбивает рабочий темп.",[11,11178,11179,11180,11185,11186,11189],{},"Чтобы решить эту проблему раз и навсегда, я разработал ",[1270,11181,11184],{"href":11182,"rel":11183},"https:\u002F\u002Fgithub.com\u002Falexrel\u002FEzSwitch",[1300],"EzSwitch"," — легкую утилиту для macOS, написанную на ",[15,11187,11188],{},"Swift и SwiftUI",", которая размещается в системном меню-баре и автоматизирует работу с клавиатурными раскладками.",[11,11191,11192],{},"В этой статье я расскажу о возможностях утилиты и о том, как реализованы ее ключевые функции под капотом macOS.",[26,11194,11196],{"id":11195},"ключевые-фичи-ezswitch","Ключевые фичи EzSwitch",[11,11198,11199],{},"Приложение решает две основные задачи: быстрое переключение раскладок физической клавишей и мгновенное исправление уже набранного не в том режиме текста.",[862,11201,11203],{"id":11202},"_1-переключение-по-клавише-command-или-option","1. Переключение по клавише Command (или Option)",[11205,11206],"blog-image",{"alt":11207,"caption":11207,"layout":11208,"src":11209,"size":11210},"Интерфейс EzSwitch в меню-баре","right","\u002Fimages\u002Fezswitch\u002FezSwitch-app.png","sm",[11,11212,11213,11214,11217,11218,11221],{},"Вместо стандартных неудобных комбинаций ",[42,11215,11216],{},"Cmd+Space"," или ",[42,11219,11220],{},"Ctrl+Space",", EzSwitch позволяет переключать язык одиночным тапом по модификаторам:",[1292,11223,11224,11231],{},[1232,11225,11226,11227,11230],{},"Одиночный тап по ",[15,11228,11229],{},"левому Command"," ➔ принудительно включает левую раскладку (например, английский).",[1232,11232,11226,11233,11236],{},[15,11234,11235],{},"правому Command"," ➔ включает правую раскладку (например, русский).\nЭто работает мгновенно и безошибочно, так как вам не нужно помнить, какой язык активен сейчас. Достаточно нажать левую или правую кнопку.",[862,11238,11240],{"id":11239},"_2-конвертация-набранного-текста-по-двойному-shift","2. Конвертация набранного текста по двойному Shift",[11,11242,11243,11244,11247,11248,11251],{},"Если вы забыли переключить язык и набрали слово «абракадаброй», выделите его и дважды нажмите клавишу ",[15,11245,11246],{},"Shift"," (или ",[15,11249,11250],{},"Option","). Текст мгновенно трансформируется в правильную раскладку.",[1292,11253,11254,11257],{},[1232,11255,11256],{},"Поддерживается исправление как выделенного текста, так и последнего слова перед курсором.",[1232,11258,11259],{},"Имеется функция автопереключения раскладки вслед за исправленным текстом.",[26,11261,11263],{"id":11262},"как-это-устроено-под-капотом-macos","Как это устроено под капотом macOS",[11,11265,11266],{},"Разработка подобного системного софта под macOS требует глубокого взаимодействия с низкоуровневыми API операционной системы.",[862,11268,11270],{"id":11269},"перехват-клавиатурных-событий-event-taps","Перехват клавиатурных событий (Event Taps)",[11,11272,11273,11274,11277,11278,11281],{},"Для отслеживания нажатий клавиш (двойной Shift, одиночный Command) в ",[42,11275,11276],{},"KeyboardManager.swift"," используется механизм ",[15,11279,11280],{},"CGEventTap",".\nПриложение регистрирует слушатель событий клавиатуры на уровне системы:",[34,11283,11287],{"className":11284,"code":11285,"language":11286,"meta":40,"style":40},"language-swift shiki shiki-themes github-dark","let eventTap = CGEvent.tapCreate(\n    tap: .cgSessionEventTap,\n    place: .headInsertEventTap,\n    options: .defaultTap,\n    eventsOfInterest: [.keyDown, .keyUp, .flagsChanged],\n    callback: myEventTapCallback,\n    userInfo: nil\n)\n","swift",[42,11288,11289,11294,11299,11304,11309,11314,11319,11324],{"__ignoreMap":40},[79,11290,11291],{"class":81,"line":82},[79,11292,11293],{},"let eventTap = CGEvent.tapCreate(\n",[79,11295,11296],{"class":81,"line":103},[79,11297,11298],{},"    tap: .cgSessionEventTap,\n",[79,11300,11301],{"class":81,"line":110},[79,11302,11303],{},"    place: .headInsertEventTap,\n",[79,11305,11306],{"class":81,"line":123},[79,11307,11308],{},"    options: .defaultTap,\n",[79,11310,11311],{"class":81,"line":139},[79,11312,11313],{},"    eventsOfInterest: [.keyDown, .keyUp, .flagsChanged],\n",[79,11315,11316],{"class":81,"line":152},[79,11317,11318],{},"    callback: myEventTapCallback,\n",[79,11320,11321],{"class":81,"line":165},[79,11322,11323],{},"    userInfo: nil\n",[79,11325,11326],{"class":81,"line":177},[79,11327,513],{},[11,11329,11330,11331,11334],{},"Это требует предоставления приложению системного разрешения ",[15,11332,11333],{},"Input Monitoring"," (Мониторинг ввода).",[862,11336,11338],{"id":11337},"автоматическая-генерация-мапперов-через-uckeytranslate","Автоматическая генерация мапперов через UCKeyTranslate",[11,11340,11341,11342,11345],{},"Чтобы текст корректно конвертировался между любыми парами языков, приложению нужна таблица соответствия клавиш. Вместо написания таких файлов вручную, EzSwitch использует Carbon API под названием ",[15,11343,11344],{},"UCKeyTranslate",".\nПриложение считывает активные раскладки в системе, симулирует виртуальные нажатия клавиш для каждой физической кнопки во всех модификациях (с Shift и без) и на лету генерирует точную карту символов. Это гарантирует, что конвертер будет корректно работать с абсолютно любыми установленными у пользователя раскладками ввода.",[11205,11347],{"alt":11348,"caption":11349,"layout":11350,"src":11351},"Сгенерированная карта раскладки клавиатуры","Сгенерированная в рантайме карта раскладки клавиатуры через Carbon UCKeyTranslate API","center","\u002Fimages\u002Fezswitch\u002FezSwitch-mapper.png",[862,11353,11355],{"id":11354},"замена-текста","Замена текста",[11,11357,11358,11359,3994],{},"Для исправления текста приложение использует симуляцию клавиатурных сочетаний через доступность (",[15,11360,11361],{},"Accessibility API",[1229,11363,11364,11374,11380,11383],{},[1232,11365,11366,11367,11247,11370,11373],{},"Симулирует нажатие ",[42,11368,11369],{},"Shift + Option + Left",[42,11371,11372],{},"Ctrl + Shift + Left"," в IDE) для выделения последнего набранного слова.",[1232,11375,11376,11377,441],{},"Копирует выделенный текст в буфер обмена (",[42,11378,11379],{},"Cmd+C",[1232,11381,11382],{},"Трансформирует символы буфера по сгенерированной карте символов.",[1232,11384,11385,11386,441],{},"Вставляет исправленный текст обратно (",[42,11387,11388],{},"Cmd+V",[26,11390,11392],{"id":11391},"заключение","Заключение",[11,11394,11395,11396,11400],{},"EzSwitch получился лёгким, быстрым и не требовательным к ресурсам инструментом, который экономит массу времени при ежедневной работе на Mac. Проект полностью открытый — переходите в ",[1270,11397,11399],{"href":11182,"rel":11398},[1300],"репозиторий EzSwitch на GitHub",", чтобы изучить исходный код, собрать его в Xcode или внести свои доработки.",[11,11402,11403,11404,11406,11407,60],{},"Разрабатываете инструмент или утилиту и нужна помощь с архитектурой или технической стороной? Можем обсудить это на ",[1270,11405,3783],{"href":1324},". Или посмотрите ",[1270,11408,11410],{"href":11409},"\u002Fru\u002Fabout","что ещё я строю и разрабатываю",[1328,11412,11413],{},"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":11415},[11416,11420,11425],{"id":11195,"depth":103,"text":11196,"children":11417},[11418,11419],{"id":11202,"depth":110,"text":11203},{"id":11239,"depth":110,"text":11240},{"id":11262,"depth":103,"text":11263,"children":11421},[11422,11423,11424],{"id":11269,"depth":110,"text":11270},{"id":11337,"depth":110,"text":11338},{"id":11354,"depth":110,"text":11355},{"id":11391,"depth":103,"text":11392},"2026-05-17","Как я разработал приложение в меню-бар на Swift\u002FSwiftUI для умного переключения языков ввода и конвертации текста по двойному клику.",null,{},"\u002Fblog\u002Fru\u002Fezswitch-macos-switcher",{"title":11163,"description":11427},"blog\u002Fru\u002Fezswitch-macos-switcher",[11434,11435,11436,11437,11438,11439,11440],"Mac","Утилита","SwiftUI","CLI","Open Source","macOS","Productivity","Cv6hDCTMvN9huHqcQbqLBBOxvFLgdpTFzUODYiNHSLw",1784561414095]