[{"data":1,"prerenderedAt":2493},["ShallowReactive",2],{"header-latest":3,"snippets-list":1368,"snippet-detail-nginx-reverse-proxy-ssl-ru":2236},[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,1380,1389,1399,1408,1416,1426,1434,1442,1452,1460,1468,1476,1484,1491,1500,1506,1513,1521,1527,1535,1541,1551,1560,1567,1575,1584,1590,1598,1606,1614,1620,1625,1631,1637,1642,1647,1653,1658,1665,1672,1678,1684,1690,1696,1702,1708,1715,1722,1729,1735,1741,1748,1755,1762,1768,1774,1780,1786,1792,1798,1804,1810,1816,1822,1828,1834,1840,1846,1852,1858,1864,1870,1876,1882,1888,1894,1900,1906,1912,1917,1923,1929,1935,1941,1947,1953,1959,1965,1971,1977,1983,1989,1995,2001,2007,2013,2019,2025,2031,2037,2042,2048,2054,2060,2066,2072,2078,2084,2090,2097,2103,2109,2115,2121,2127,2137,2143,2148,2155,2161,2167,2173,2179,2185,2191,2198,2205,2212,2218,2224,2230],{"path":1370,"title":1371,"description":1372,"language":1373,"tags":1374,"date":1379,"lang":1356},"\u002Fsnippets\u002Fru\u002Freact-context-usereducer","React context + useReducer для глобального состояния","Замена библиотеки управления состоянием на Context и useReducer — предсказуемый, тестируемый паттерн без внешних зависимостей.","React.js",[1375,1376,1377,1378],"React","Context","useReducer","State Management","2026-08-07",{"path":1381,"title":1382,"description":1383,"language":1373,"tags":1384,"date":1388,"lang":1356},"\u002Fsnippets\u002Fru\u002Freact-usemediaquery-hook","React useMediaQuery — адаптивный хук","Отслеживание CSS media queries в React-компонентах — определение брейкпоинтов, разный UI для мобильных\u002Fпланшетов\u002Fдесктопа и реакция на смену ориентации.",[1375,1385,1386,1387],"Hooks","Responsive","Media Query","2026-08-06",{"path":1390,"title":1391,"description":1392,"language":1393,"tags":1394,"date":1398,"lang":1356},"\u002Fsnippets\u002Fru\u002Ftailwind-animations-keyframes","Tailwind CSS анимации — keyframes и transition","Создание кастомных анимаций через `animate-*` и `@keyframes` в Tailwind — от микро-взаимодействий до переходов между страницами без выхода из конфига.","Tailwind CSS",[1393,1395,1396,1397],"Animation","Keyframes","UX","2026-08-05",{"path":1400,"title":1401,"description":1402,"language":1393,"tags":1403,"date":1407,"lang":1356},"\u002Fsnippets\u002Fru\u002Ftailwind-custom-theme-variables","Tailwind CSS кастомная тема через CSS переменные","Расширение стандартной темы Tailwind кастомными цветами, шрифтами, отступами и брейкпоинтами — все значения через CSS-переменные для смены темы в рантайме.",[1393,1404,1405,1406],"Theme","Customization","Design System","2026-08-04",{"path":1409,"title":1410,"description":1411,"language":1393,"tags":1412,"date":1415,"lang":1356},"\u002Fsnippets\u002Fru\u002Ftailwind-dark-mode-class","Tailwind CSS dark mode со стратегией class","Включение тёмной темы по кнопке вместо системных настроек — стратегия class даёт полный контроль через переключатель с сохранением в localStorage.",[1393,1413,1404,1414],"Dark Mode","Styling","2026-08-03",{"path":1417,"title":1418,"description":1419,"language":1420,"tags":1421,"date":1425,"lang":1356},"\u002Fsnippets\u002Fru\u002Fprisma-migrations-workflow","Prisma миграции — создание, применение и откат","Управление изменениями схемы базы данных через Prisma Migrate — создание миграций из изменений схемы, применение на продакшене и обработка рассинхронизации.","Prisma",[1420,1422,1423,1424],"ORM","Migrations","Database","2026-08-02",{"path":1427,"title":1428,"description":1429,"language":1420,"tags":1430,"date":1433,"lang":1356},"\u002Fsnippets\u002Fru\u002Fprisma-pagination-cursor-offset","Prisma пагинация — cursor и offset","Эффективная пагинация больших наборов данных через Prisma — offset для простых списков, cursor для infinite scroll и real-time лент.",[1420,1422,1431,1432],"Pagination","Performance","2026-08-01",{"path":1435,"title":1436,"description":1437,"language":1420,"tags":1438,"date":1441,"lang":1356},"\u002Fsnippets\u002Fru\u002Fprisma-crud-operations","Prisma CRUD — создание, чтение, обновление, удаление","Четыре базовые операции с данными через Prisma Client — создать пользователя, найти по email, обновить профиль, удалить аккаунт — всё с полной типизацией.",[1420,1422,1439,1440],"CRUD","TypeScript","2026-07-31",{"path":1443,"title":1444,"description":1445,"language":1446,"tags":1447,"date":1451,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnginx-websocket-proxy","Nginx WebSocket прокси для real-time приложений","Проксирование WebSocket-соединений через nginx — необходимо для Socket.io, живого чата и совместного редактирования за reverse proxy.","Nginx",[1446,1448,1449,1450],"WebSocket","Proxy","Real-time","2026-07-30",{"path":1453,"title":1454,"description":1455,"language":1446,"tags":1456,"date":1459,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnginx-load-balancing-nodejs","Nginx load balancing между Node.js инстансами","Распределение трафика между несколькими Node.js бэкендами с round-robin, проверками здоровья и пассивным failover — без изменений в приложении.",[1446,1457,1365,1458],"Load Balancing","DevOps","2026-07-29",{"path":1461,"title":1462,"description":1463,"language":1446,"tags":1464,"date":1467,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnginx-static-cache-server","Nginx статический сервер с кешированием","Раздача статики (CSS, JS, изображения) с заголовками кеширования на год, gzip-сжатием и SPA-роутингом — без серверного приложения.",[1446,1465,1466,1432],"Static Files","Caching","2026-07-28",{"path":1469,"title":1470,"description":1471,"language":1472,"tags":1473,"date":1475,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgithub-actions-scheduled-cron","GitHub Actions — запланированные задачи по cron","Запуск воркфлоу по расписанию — ежедневные бэкапы, еженедельные обновления зависимостей, почасовые проверки здоровья — через cron-синтаксис.","GitHub Actions",[1472,1474,1458],"Automation","2026-07-27",{"path":1477,"title":1478,"description":1479,"language":1472,"tags":1480,"date":1483,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgithub-actions-docker-build-push","GitHub Actions — сборка и публикация Docker образа","Автоматическая сборка и публикация Docker образа в Docker Hub или GitHub Container Registry на каждый push в main — с кешированием слоёв.",[1472,1481,1482,1458],"Docker","CI\u002FCD","2026-07-26",{"path":1485,"title":1486,"description":1487,"language":1472,"tags":1488,"date":1490,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgithub-actions-ci-pr","GitHub Actions — CI пайплайн для PR","Запуск линтинга, type-check и тестов на каждый pull request до мержа — быстрый отказ с понятными аннотациями в PR.",[1472,1482,1489,1458],"Testing","2026-07-25",{"path":1492,"title":1493,"description":1494,"language":1495,"tags":1496,"date":1499,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgit-log-filter-format","Git log — фильтрация и форматирование истории","Поиск, фильтрация и форматирование Git-лога — находите коммиты по автору, дате, файлу или содержимому одной строкой.","Git",[1495,1497,1498],"Workflow","Debugging","2026-07-24",{"path":1501,"title":1502,"description":1503,"language":1495,"tags":1504,"date":1505,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgit-cherry-pick-bisect","Git cherry-pick и bisect — точечная работа с коммитами","Cherry-pick применяет конкретный коммит из любой ветки. Bisect — бинарный поиск по истории для нахождения коммита, внёсшего баг.",[1495,1497,1498],"2026-07-23",{"path":1507,"title":1508,"description":1509,"language":1495,"tags":1510,"date":1512,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgit-stash-work-in-progress","Git stash — сохранение и восстановление черновиков","Временно сохраните незакоммиченные изменения через git stash — переключайте ветки, подтягивайте обновления и восстанавливайте работу без потерь.",[1495,1497,1511],"Version Control","2026-07-22",{"path":1514,"title":1515,"description":1516,"language":1481,"tags":1517,"date":1520,"lang":1356},"\u002Fsnippets\u002Fru\u002Fdocker-security-nonroot","Docker безопасность — non-root пользователь и read-only","Усиление безопасности контейнеров — запуск от непривилегированного пользователя с read-only файловой системой и отключением возможностей ядра.",[1481,1518,1458,1519],"Security","Production","2026-07-21",{"path":1522,"title":1523,"description":1524,"language":1481,"tags":1525,"date":1526,"lang":1356},"\u002Fsnippets\u002Fru\u002Fdocker-multistage-build-nodejs","Docker multi-stage сборки для Node.js","Production Dockerfile с multi-stage сборкой — раздельные стадии для зависимостей, сборки и рантайма — минимальный финальный образ.",[1481,1365,1458,1519],"2026-07-20",{"path":1528,"title":1529,"description":1530,"language":1393,"tags":1531,"date":1534,"lang":1356},"\u002Fsnippets\u002Fru\u002Ftailwind-responsive-card-grid","Tailwind CSS адаптивная сетка карточек","Адаптивная сетка карточек на Tailwind CSS — от 1 колонки на мобильных до 4 на десктопе — без медиа-запросов.",[1393,1532,1386,1533],"CSS","Grid","2026-07-19",{"path":1536,"title":1537,"description":1538,"language":1495,"tags":1539,"date":1540,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgit-squash-rebase-workflow","Git squash коммитов и интерактивный rebase","Очистка Git-истории с помощью squash, fixup и интерактивного rebase — читаемый лог коммитов перед мержем.",[1495,1497,1511],"2026-07-18",{"path":1542,"title":1543,"description":1544,"language":1545,"tags":1546,"date":1550,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnextjs-middleware-auth","Next.js App Router middleware — аутентификация и редиректы","Защита маршрутов через Next.js middleware — проверка auth-токенов, редирект неавторизованных пользователей, i18n-роутинг.","Next.js",[1545,1547,1548,1549],"Auth","Middleware","App Router","2026-07-17",{"path":1552,"title":1553,"description":1554,"language":1365,"tags":1555,"date":1559,"lang":1356},"\u002Fsnippets\u002Fru\u002Fstripe-webhook-idempotency","Stripe webhook handler с идемпотентностью","Безопасная обработка Stripe-вебхуков — проверка подписи, дедупликация событий, обработка checkout.session.completed и обновлений подписок.",[1365,1556,1557,1558],"Stripe","Webhooks","Payments","2026-07-16",{"path":1561,"title":1562,"description":1563,"language":1472,"tags":1564,"date":1566,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgithub-actions-deploy-vps","GitHub Actions — деплой Node.js на VPS через SSH","CI\u002FCD пайплайн, который линтит, тестирует, собирает и деплоит Node.js приложение на VPS через SSH с zero-downtime.",[1472,1482,1458,1565],"Deployment","2026-07-15",{"path":1568,"title":1569,"description":1570,"language":1571,"tags":1572,"date":1574,"lang":1356},"\u002Fsnippets\u002Fru\u002Fexpress-jwt-refresh-tokens","JWT access и refresh token rotation в Express","Полноценная JWT-аутентификация с короткоживущим access токеном и ротацией httpOnly refresh токена — без уязвимости повторного использования.","Express.js",[1571,1573,1547,1518],"JWT","2026-07-14",{"path":1576,"title":1577,"description":1578,"language":1440,"tags":1579,"date":1583,"lang":1356},"\u002Fsnippets\u002Fru\u002Fzod-api-validation-schema","Zod схемы валидации для API","Type-safe валидация запросов с Zod — парсинг, трансформация и проверка тела запроса, query-параметров и URL-параметров.",[1440,1580,1581,1582],"Zod","Validation","API","2026-07-13",{"path":1585,"title":1586,"description":1587,"language":1420,"tags":1588,"date":1589,"lang":1356},"\u002Fsnippets\u002Fru\u002Fprisma-schema-user-relations","Prisma схема — User, Post и связи","Практическая Prisma-схема с моделями User, Post, Session и Subscription, использующая связи, перечисления и индексы.",[1420,1422,1424,1440],"2026-07-12",{"path":1591,"title":1592,"description":1593,"language":1446,"tags":1594,"date":1597,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnginx-reverse-proxy-ssl","Nginx reverse proxy с SSL termination","Hardened-конфигурация nginx для проксирования Node.js приложений — SSL termination, rate limiting, заголовки безопасности и gzip.",[1446,1458,1595,1596],"SSL","Reverse Proxy","2026-07-11",{"path":1599,"title":1600,"description":1601,"language":1481,"tags":1602,"date":1605,"lang":1356},"\u002Fsnippets\u002Fru\u002Fdocker-compose-node-postgres-redis","Docker Compose для Node.js + PostgreSQL + Redis","Production-ready docker-compose.yml для Node.js приложения с PostgreSQL и Redis — стандартный стек SaaS-бэкенда.",[1481,1458,1365,1603,1604],"PostgreSQL","Redis","2026-07-10",{"path":1607,"title":1608,"description":1609,"language":1610,"tags":1611,"date":1613,"lang":1356},"\u002Fsnippets\u002Fru\u002Fvue-use-view-mode","Хук useViewMode","Переключение между сеткой и списком с сохранением выбора в localStorage между визитами.","Vue",[1610,1612,1385],"JavaScript","2026-07-09",{"path":1615,"title":1616,"description":1617,"language":1610,"tags":1618,"date":1619,"lang":1356},"\u002Fsnippets\u002Fru\u002Fvue-use-platform","Хук usePlatform","Определяет macOS среди других платформ, чтобы показывать правильный символ горячей клавиши (⌘ или Ctrl).",[1610,1612,1385],"2026-07-08",{"path":1621,"title":1622,"description":1623,"language":1610,"tags":1624,"date":1619,"lang":1356},"\u002Fsnippets\u002Fru\u002Fvue-use-theme","Хук useTheme","Vue-композабл для определения и переключения светлой\u002Fтёмной\u002Fсистемной темы с сохранением в localStorage.",[1610,1612,1385],{"path":1626,"title":1627,"description":1628,"language":1610,"tags":1629,"date":1630,"lang":1356},"\u002Fsnippets\u002Fru\u002Fvue-use-media-query","useMediaQuery — реактивный media query","Vue-композабл, который возвращает реактивный ref, обновляемый при изменении результата CSS media query. SSR-безопасный — не падает на сервере.",[1610,1612,1385,1532],"2026-07-07",{"path":1632,"title":1633,"description":1634,"language":1610,"tags":1635,"date":1636,"lang":1356},"\u002Fsnippets\u002Fru\u002Fvue-use-drag-and-drop","Хук useDragAndDrop","Vue-композабл для drag-and-drop загрузки файлов — отслеживает состояние перетаскивания и передаёт брошенные файлы в ваш колбэк.",[1610,1612,1385],"2026-07-06",{"path":1638,"title":1639,"description":1640,"language":1610,"tags":1641,"date":1636,"lang":1356},"\u002Fsnippets\u002Fru\u002Fvue-use-intersection-observer","useIntersectionObserver — Vue composable","Vue-композабл для отслеживания видимости элемента в viewport — подходит для lazy-load изображений, анимаций при скролле и infinite scroll.",[1610,1612,1385,1432],{"path":1643,"title":1644,"description":1645,"language":1610,"tags":1646,"date":1342,"lang":1356},"\u002Fsnippets\u002Fru\u002Fvue-use-debounced-ref","useDebouncedRef — отложенное реактивное значение","Vue-композабл, возвращающий пару ref: «живое» значение и debounced-версию, которая обновляется только после паузы в вводе. Идеально для поисковых запросов и фильтров.",[1610,1612,1385,1432],{"path":1648,"title":1649,"description":1650,"language":1610,"tags":1651,"date":1652,"lang":1356},"\u002Fsnippets\u002Fru\u002Fvue-use-breakpoints","Хук useBreakpoints","Vue-композабл, который реактивно отслеживает ширину экрана и отдаёт флаги isMobile\u002FisTablet\u002FisDesktop.",[1610,1612,1385],"2026-07-04",{"path":1654,"title":1655,"description":1656,"language":1610,"tags":1657,"date":1652,"lang":1356},"\u002Fsnippets\u002Fru\u002Fvue-use-clipboard-copy","Хук useClipboardCopy","Vue-композабл, который копирует текст в буфер обмена с фолбэком для старых браузеров и коротким состоянием \"скопировано\".",[1610,1612,1385],{"path":1659,"title":1660,"description":1661,"language":1440,"tags":1662,"date":1664,"lang":1356},"\u002Fsnippets\u002Fru\u002Ftypescript-use-fetch-with-abort","useFetch с AbortController — TypeScript","Vue-композабл для fetch-запросов с автоматической отменой предыдущего запроса при новом вызове. Реактивные состояния data, loading и error из коробки.",[1440,1610,1385,1582,1663],"Async","2026-07-03",{"path":1666,"title":1667,"description":1668,"language":1440,"tags":1669,"date":1671,"lang":1356},"\u002Fsnippets\u002Fru\u002Ftypescript-pagination-offset-limit","Утилиты пагинации (offset\u002Flimit)","Типизированные вспомогательные функции для серверной пагинации — вычисляют offset и limit из номера страницы, возвращают метаданные (total pages, hasNext, hasPrev).",[1440,1670,1582],"Utils","2026-07-02",{"path":1673,"title":1674,"description":1675,"language":1440,"tags":1676,"date":1677,"lang":1356},"\u002Fsnippets\u002Fru\u002Ftypescript-group-by-key","Группировка массива по ключу","Аналог lodash groupBy без зависимостей — группирует массив объектов по значению заданного поля в объект Map\u002FRecord. TypeScript-типизированная версия.",[1612,1440,1670],"2026-07-01",{"path":1679,"title":1680,"description":1681,"language":1440,"tags":1682,"date":1683,"lang":1356},"\u002Fsnippets\u002Fru\u002Ftypescript-chunk-array","Разбить массив на чанки","Разделяет массив на группы заданного размера — удобно для батчевой обработки, пагинации в памяти или отправки данных частями.",[1612,1440,1670],"2026-06-30",{"path":1685,"title":1686,"description":1687,"language":1373,"tags":1688,"date":1689,"lang":1356},"\u002Fsnippets\u002Fru\u002Freact.js-use-local-storage-react","Хук usePersistedState","Замена useState, которая синхронизирует значение с localStorage, чтобы оно сохранялось после перезагрузки страницы.",[1373,1612,1385],"2026-06-29",{"path":1691,"title":1692,"description":1693,"language":1373,"tags":1694,"date":1695,"lang":1356},"\u002Fsnippets\u002Fru\u002Freact.js-use-fetch-hook","Хук useFetch","Переиспользуемый хук для загрузки данных с состояниями loading\u002Ferror, отменяющий запрос при размонтировании компонента или изменении URL.",[1373,1612,1385],"2026-06-28",{"path":1697,"title":1698,"description":1699,"language":1373,"tags":1700,"date":1701,"lang":1356},"\u002Fsnippets\u002Fru\u002Freact.js-use-debounce-hook","Хук useDebounce","React-хук, который делает дебаунс быстро меняющегося значения (например, поискового ввода), чтобы эффекты запускались только после того, как пользователь перестал печатать.",[1373,1612,1385],"2026-06-27",{"path":1703,"title":1704,"description":1705,"language":1373,"tags":1706,"date":1707,"lang":1356},"\u002Fsnippets\u002Fru\u002Freact.js-use-click-outside-hook","Хук useOnClickOutside","Определяет клик снаружи заданного элемента — стандартный способ закрывать дропдауны, модалки и поповеры.",[1373,1612,1385],"2026-06-26",{"path":1709,"title":1710,"description":1711,"language":1712,"tags":1713,"date":1714,"lang":1356},"\u002Fsnippets\u002Fru\u002Fpython-retry-decorator-python","Декоратор retry с backoff (Python)","Декоратор для повтора нестабильных функций с экспоненциальной задержкой и джиттером. Настраиваемые исключения, количество попыток и задержка.","Python",[1712,1670,1582,1663],"2026-06-25",{"path":1716,"title":1717,"description":1718,"language":1712,"tags":1719,"date":1721,"lang":1356},"\u002Fsnippets\u002Fru\u002Fpython-read-csv-file","Чтение CSV-файла в список словарей","Стандартный способ разобрать заголовок CSV-файла в ключи, используя встроенный модуль csv в Python — pandas не нужен для простых случаев.",[1712,1720],"Files","2026-06-24",{"path":1723,"title":1724,"description":1725,"language":1712,"tags":1726,"date":1728,"lang":1356},"\u002Fsnippets\u002Fru\u002Fpython-flatten-nested-list-python","Разворачивание вложенного списка","Рекурсивно превращает произвольно вложенный список в один плоский список.",[1712,1727],"Utility","2026-06-23",{"path":1730,"title":1731,"description":1732,"language":1712,"tags":1733,"date":1734,"lang":1356},"\u002Fsnippets\u002Fru\u002Fpython-check-file-exists","Проверка существования файла или директории","Современный кросс-платформенный способ проверить существование файла через pathlib вместо старых функций os.path.",[1712,1720],"2026-06-22",{"path":1736,"title":1737,"description":1738,"language":1712,"tags":1739,"date":1740,"lang":1356},"\u002Fsnippets\u002Fru\u002Fpython-async-gather-limited-python","asyncio.gather с ограничением concurrency","Запускает список async-задач параллельно, но не более N одновременно — через asyncio.Semaphore. Предотвращает перегрузку внешних API или БД при большом числе задач.",[1712,1663,1670,1582],"2026-06-21",{"path":1742,"title":1743,"description":1744,"language":1745,"tags":1746,"date":1747,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-validate-email","Валидация email-адреса","Правильный встроенный способ проверить формат email в PHP — без самодельного регулярного выражения.","PHP",[1745,1727],"2026-06-20",{"path":1749,"title":1750,"description":1751,"language":1745,"tags":1752,"date":1754,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-timezone-gmt-offset","Получение текущего GMT-смещения часового пояса в PHP","Превращает название IANA-часового пояса в удобную метку вроде \"(GMT+02:00) Asia\u002FJerusalem\" с учётом перехода на летнее время.",[1745,1727,1753],"i18n","2026-06-19",{"path":1756,"title":1757,"description":1758,"language":1745,"tags":1759,"date":1761,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-telegram-webhook-verify-secret","Верификация Telegram Webhook Secret","Проверяет заголовок X-Telegram-Bot-Api-Secret-Token входящего вебхук-запроса — защищает эндпоинт от посторонних запросов не из Telegram.",[1745,1362,1760,1518],"Bots","2026-06-18",{"path":1763,"title":1764,"description":1765,"language":1745,"tags":1766,"date":1767,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-telegram-send-message-php","Отправка сообщения через Telegram Bot API (PHP)","Отправляет сообщение через Telegram Bot API без SDK — только встроенный curl. Поддерживает MarkdownV2, ReplyMarkup, reply_to_message_id и любые другие параметры sendMessage.",[1745,1362,1760,1582],"2026-06-17",{"path":1769,"title":1770,"description":1771,"language":1745,"tags":1772,"date":1773,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-telegram-get-user-php","Получить ID пользователя Telegram через PHP-вебхук","Читает входящий Telegram-апдейт из php:\u002F\u002Finput и возвращает нормализованный объект пользователя — id, username, имя и код языка. Работает с message, callback_query и другими типами.",[1745,1362,1760],"2026-06-16",{"path":1775,"title":1776,"description":1777,"language":1745,"tags":1778,"date":1779,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-secure-random-password-php","Генерация безопасного случайного пароля","Создаёт криптографически безопасный случайный пароль с помощью random_bytes() и bin2hex() в PHP.",[1745,1518],"2026-06-15",{"path":1781,"title":1782,"description":1783,"language":1745,"tags":1784,"date":1785,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-sanitize-utf8-php","Приведение строки к валидному UTF-8","Убирает или исправляет некорректные последовательности байтов, чтобы строку всегда можно было безопасно сохранить, залогировать или вернуть как JSON.",[1745,1727],"2026-06-14",{"path":1787,"title":1788,"description":1789,"language":1745,"tags":1790,"date":1791,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-retry-with-backoff","Повтор операции с экспоненциальной задержкой и джиттером","Повторяет нестабильный вызов API с увеличивающимися задержками между попытками плюс случайный джиттер, чтобы много клиентов не повторяли запрос синхронно.",[1745,1582],"2026-06-13",{"path":1793,"title":1794,"description":1795,"language":1745,"tags":1796,"date":1797,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-rate-limiter-redis","Rate Limiting через Redis (PHP)","Простой rate limiter на Redis — ограничивает количество запросов от пользователя за скользящее временное окно. Атомарный инкремент через INCR\u002FEXPIRE без дополнительных зависимостей.",[1745,1604,1582,1518],"2026-06-12",{"path":1799,"title":1800,"description":1801,"language":1745,"tags":1802,"date":1803,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-parse-bcp47-language-tag","Разбор языкового тега BCP-47","Приводит языковые коды вроде en-GB, pt_BR или sr_RS (как их присылают браузеры, Telegram и т.д.) к паре язык + регион.",[1745,1727],"2026-06-11",{"path":1805,"title":1806,"description":1807,"language":1745,"tags":1808,"date":1809,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-mask-token-preview","Маскирование токена\u002FAPI-ключа для безопасного отображения","Показывает только первые и последние 4 символа секрета (например, API-ключа), чтобы его можно было безопасно логировать или отображать в интерфейсе.",[1745,1518],"2026-06-10",{"path":1811,"title":1812,"description":1813,"language":1745,"tags":1814,"date":1815,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-ip-geolocation-php","Получение геолокации по IP (ipapi.co)","Определяет город и страну посетителя по его IP-адресу через бесплатный API ipapi.co.",[1745,1582],"2026-06-09",{"path":1817,"title":1818,"description":1819,"language":1745,"tags":1820,"date":1821,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-initials-avatar-php","Цветной аватар с инициалами (HTML\u002FCSS)","Рендерит круглый аватар с инициалами пользователя и цветом, полученным из его имени — лёгкий фолбэк, когда нет фото профиля.",[1745,1727],"2026-06-08",{"path":1823,"title":1824,"description":1825,"language":1745,"tags":1826,"date":1827,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-hash-password-bcrypt-php","Хеширование паролей bcrypt (PHP)","Хеширует и проверяет пароли через нативный PHP password_hash с bcrypt (cost 12). Включает проверку необходимости повторного хеширования при изменении параметров.",[1745,1518,1547],"2026-06-07",{"path":1829,"title":1830,"description":1831,"language":1745,"tags":1832,"date":1833,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-get-client-ip-behind-proxy","Получение реального IP клиента за прокси\u002FCDN","Читает настоящий IP посетителя из заголовков CDN\u002Fпрокси (Cloudflare, X-Forwarded-For и т.д.), с фолбэком на $_SERVER['REMOTE_ADDR'], и отбрасывает приватные\u002Fподдельные значения.",[1745,1518],"2026-06-06",{"path":1835,"title":1836,"description":1837,"language":1745,"tags":1838,"date":1839,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-format-bytes-human-readable","Форматирование байтов в читаемый размер","Превращает количество байтов в \"1.5 MB\", \"820 KB\" и т.д. — стандартный хелпер для отображения размера файла.",[1745,1727],"2026-06-05",{"path":1841,"title":1842,"description":1843,"language":1745,"tags":1844,"date":1845,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-escape-telegram-markdownv2","Экранирование текста для Telegram MarkdownV2","Bot API Telegram отклоняет сообщения с неэкранированными спецсимволами MarkdownV2 — эта функция безопасно экранирует их все.",[1745,1582,1362],"2026-06-04",{"path":1847,"title":1848,"description":1849,"language":1745,"tags":1850,"date":1851,"lang":1356},"\u002Fsnippets\u002Fru\u002Fphp-curl-get-request","GET-запрос через cURL","Классический рецепт PHP для получения JSON от внешнего API без каких-либо дополнительных зависимостей.",[1745,1582],"2026-06-03",{"path":1853,"title":1854,"description":1855,"language":1365,"tags":1856,"date":1857,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnode.js-timing-safe-string-compare","Сравнение строк за постоянное время для секретов","Сравнивает два секрета (API-токены, подписи вебхуков) не раскрывая информацию о времени выполнения, которая могла бы помочь атакующему подобрать их посимвольно.",[1365,1518],"2026-06-02",{"path":1859,"title":1860,"description":1861,"language":1365,"tags":1862,"date":1863,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnode.js-read-file-async","Асинхронное чтение файла","Современный способ на промисах прочитать содержимое файла в Node.js — без коллбэков и без блокировки event loop, в отличие от fs.readFileSync.",[1365,1720],"2026-06-01",{"path":1865,"title":1866,"description":1867,"language":1365,"tags":1868,"date":1869,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnode.js-read-env-defaults-node","Чтение переменных окружения со значением по умолчанию","Небольшой хелпер, который читает переменную окружения Node.js и возвращает значение по умолчанию, если она не задана.",[1365,1612],"2026-05-31",{"path":1871,"title":1872,"description":1873,"language":1365,"tags":1874,"date":1875,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnode.js-hash-password-bcrypt","Хеширование и проверка пароля через bcrypt","Стандартный способ безопасно хранить пароли в Node.js — никогда не храните и не сравнивайте их в открытом виде.",[1365,1518],"2026-05-30",{"path":1877,"title":1878,"description":1879,"language":1365,"tags":1880,"date":1881,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnode.js-create-http-server","Создание простого HTTP-сервера","Способ обслуживать запросы средствами самого Node.js — Express не нужен для чего-то настолько простого.",[1365,1582],"2026-05-29",{"path":1883,"title":1884,"description":1885,"language":1545,"tags":1886,"date":1887,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnext.js-redirect","Редирект из Server Component или Route Handler","Встроенный в App Router способ отправить пользователя на другую страницу — без ручного создания Response.",[1545,1373],"2026-05-28",{"path":1889,"title":1890,"description":1891,"language":1545,"tags":1892,"date":1893,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnext.js-read-server-search-params-nextjs","Чтение search params в серверном компоненте","Доступ к текущей query-строке URL внутри Server Component в App Router без клиентского JavaScript.",[1545,1373],"2026-05-27",{"path":1895,"title":1896,"description":1897,"language":1545,"tags":1898,"date":1899,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnext.js-dynamic-metadata","Генерация динамических метаданных страницы","Задаёт \u003Ctitle> и meta-теги страницы на основе загруженных данных — замена next\u002Fhead в App Router.",[1545,1373],"2026-05-26",{"path":1901,"title":1902,"description":1903,"language":1545,"tags":1904,"date":1905,"lang":1356},"\u002Fsnippets\u002Fru\u002Fnext.js-api-route-handler","Создание обработчика API-роута","Способ App Router создать REST-эндпоинт — экспортируйте функцию с именем HTTP-метода вместо одного универсального обработчика.",[1545,1582],"2026-05-25",{"path":1907,"title":1908,"description":1909,"language":1612,"tags":1910,"date":1911,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-validate-media-url","Валидация медиа-URL","Проверяет, что URL использует HTTPS и указывает на распознаваемое расширение изображения\u002Fвидео\u002Fаудио — частая проверка перед принятием ссылки на медиа от пользователя.",[1612,1727],"2026-05-24",{"path":1913,"title":1743,"description":1914,"language":1612,"tags":1915,"date":1916,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-validate-email","Проверяет корректность email без переусложнённого regex — достаточно строго для форм и API, не выкидывает редкие, но валидные адреса.",[1612,1440,1670,1581],"2026-05-23",{"path":1918,"title":1919,"description":1920,"language":1612,"tags":1921,"date":1922,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-truncate-text-by-words","Обрезать текст по количеству слов","Урезает строку до заданного числа слов и добавляет суффикс («…» по умолчанию). В отличие от обрезки по символам, не разрывает слова посередине.",[1612,1440,1670],"2026-05-22",{"path":1924,"title":1925,"description":1926,"language":1612,"tags":1927,"date":1928,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-to-boolean","Преобразование произвольных значений в boolean","Превращает строки вроде \"yes\"\u002F\"on\"\u002F\"1\" (и числа) в настоящий boolean — удобно для парсинга env-переменных, query-параметров или CSV-импорта.",[1612,1727],"2026-05-21",{"path":1930,"title":1931,"description":1932,"language":1612,"tags":1933,"date":1934,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-timezone-gmt-offset-label","Отображение часового пояса со смещением GMT","Превращает название IANA-часового пояса в удобную метку вроде \"(GMT+02:00) Asia\u002FJerusalem\" для селекторов часовых поясов.",[1612,1727,1753],"2026-05-20",{"path":1936,"title":1937,"description":1938,"language":1612,"tags":1939,"date":1940,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-throttle-function","Троттлинг функции","Гарантирует, что функция выполняется не чаще, чем раз в X миллисекунд — идеально для обработчиков scroll и mousemove, которые иначе срабатывали бы сотни раз в секунду.",[1612,1727],"2026-05-19",{"path":1942,"title":1943,"description":1944,"language":1612,"tags":1945,"date":1946,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-telegram-user-link","Ссылка на пользователя Telegram и mention","Генерирует кликабельную ссылку на пользователя Telegram и mention-разметку для MarkdownV2 — через username если есть, иначе через tg:\u002F\u002Fuser?id= deep link.",[1612,1362,1760],"2026-05-18",{"path":1948,"title":1949,"description":1950,"language":1612,"tags":1951,"date":1952,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-telegram-get-user-from-update","Получить ID и данные пользователя из Telegram-апдейта","Извлекает ID, имя и username пользователя из любого типа входящего Telegram-апдейта — message, callback_query, edited_message, inline_query и других.",[1612,1362,1760],"2026-05-17",{"path":1954,"title":1955,"description":1956,"language":1612,"tags":1957,"date":1958,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-telegram-answer-callback-query","Ответ на callback_query Telegram","Отвечает на нажатие инлайн-кнопки (answerCallbackQuery) без SDK — убирает лоадер с кнопки, опционально показывает уведомление или alert пользователю.",[1612,1362,1760,1582],"2026-05-16",{"path":1960,"title":1961,"description":1962,"language":1612,"tags":1963,"date":1964,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-smart-telegram-button-layout","Автоматическая раскладка инлайн-кнопок Telegram","Упаковывает короткие кнопки по две в ряд, а длинным даёт отдельный ряд — в зависимости от длины текста сообщения над ними, сохраняя клавиатуру компактной без обрезания подписей.",[1612,1362],"2026-05-15",{"path":1966,"title":1967,"description":1968,"language":1612,"tags":1969,"date":1970,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-sleep-delay-async","sleep() \u002F задержка в async\u002Fawait","Минималистичная функция sleep для JavaScript и TypeScript — приостанавливает выполнение async-функции на заданное количество миллисекунд без сторонних библиотек.",[1612,1440,1670,1663],"2026-05-14",{"path":1972,"title":1973,"description":1974,"language":1612,"tags":1975,"date":1976,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-seconds-until-utc-rollover","Секунды до следующей смены суток\u002Fмесяца по UTC","Вычисляет, сколько секунд осталось до полуночи UTC (или до 1-го числа следующего месяца) — именно такой TTL нужен для ключа Redis с дневной\u002Fмесячной квотой.",[1612,1727],"2026-05-13",{"path":1978,"title":1979,"description":1980,"language":1612,"tags":1981,"date":1982,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-scroll-to-anchor","Плавный скролл к якорю","Прокручивает страницу к элементу по ID с учётом смещения (например, высоты fixed-хедера) — без jQuery, нативный smooth scroll.",[1612,1532,1670],"2026-05-12",{"path":1984,"title":1985,"description":1986,"language":1612,"tags":1987,"date":1988,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-safe-json-parse","JSON.parse, который никогда не бросает исключение","Оборачивает JSON.parse так, чтобы при некорректных данных возвращался null вместо падения — идеально для десериализации значений из Redis, localStorage или пользовательского ввода.",[1612,1727],"2026-05-11",{"path":1990,"title":1991,"description":1992,"language":1612,"tags":1993,"date":1994,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-remove-duplicate-array-items","Удаление дублей из массива","Способ в одну строку убрать дубли из массива примитивов через Set, плюс паттерн для удаления дублей массива объектов по ключу.",[1612,1727],"2026-05-10",{"path":1996,"title":1997,"description":1998,"language":1612,"tags":1999,"date":2000,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-pluralize-intl","Склонение слов через Intl.PluralRules","Корректно склоняет существительные для любой локали — включая языки вроде русского с несколькими формами множественного числа — через встроенный Intl API.",[1612,1727,1753],"2026-05-09",{"path":2002,"title":2003,"description":2004,"language":1612,"tags":2005,"date":2006,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-pick-omit-object-keys","Выбор и исключение ключей объекта","Два небольших хелпера для создания нового объекта только с указанными ключами (или без них) — удобно для обрезки payload перед отправкой в API.",[1612,1727],"2026-05-08",{"path":2008,"title":2009,"description":2010,"language":1612,"tags":2011,"date":2012,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-parse-telegram-start-params","Разбор deep-link параметров Telegram \u002Fstart","Извлекает источник трафика, UTM-метки или ID реферера из payload, который Telegram передаёт после \u002Fstart — стандартный способ отслеживать, откуда пришли пользователи бота.",[1612,1362],"2026-05-07",{"path":2014,"title":2015,"description":2016,"language":1612,"tags":2017,"date":2018,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-jwt-decode-payload","Декодировать payload JWT без верификации","Извлекает payload из JWT-токена на клиенте без проверки подписи — удобно для чтения exp, userId и ролей в UI. Никогда не использовать для авторизации на сервере.",[1612,1440,1547,1573],"2026-05-06",{"path":2020,"title":2021,"description":2022,"language":1612,"tags":2023,"date":2024,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-iso-date-without-timezone-shift","Date в\u002Fиз \"YYYY-MM-DD\" без сдвига часового пояса","date.toISOString().slice(0,10) незаметно переводит дату в UTC и может показать вчерашний день ближе к полуночи — эти два хелпера остаются в локальном времени.",[1612,1727],"2026-05-05",{"path":2026,"title":2027,"description":2028,"language":1612,"tags":2029,"date":2030,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-is-editable-target","Проверка, является ли цель события редактируемым полем","Защищает глобальные горячие клавиши от срабатывания, пока пользователь печатает в input, textarea, select или contenteditable-элементе.",[1612,1727],"2026-05-04",{"path":2032,"title":2033,"description":2034,"language":1612,"tags":2035,"date":2036,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-is-color-light","Проверка, светлый цвет или тёмный (hex)","Вычисляет воспринимаемую яркость hex-цвета, чтобы определить, какой текст лучше читается поверх него — чёрный или белый.",[1612,1727],"2026-05-03",{"path":2038,"title":1812,"description":2039,"language":1612,"tags":2040,"date":2041,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-ip-geolocation-js","Определяет страну и город посетителя по его IP-адресу через бесплатный API ipapi.co.",[1612,1582],"2026-05-02",{"path":2043,"title":2044,"description":2045,"language":1612,"tags":2046,"date":2047,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-generate-uuid","Генерация UUID","Создаёт случайный UUID (v4) по спецификации, используя встроенный Web Crypto API — без внешних библиотек.",[1612,1727],"2026-05-01",{"path":2049,"title":2050,"description":2051,"language":1612,"tags":2052,"date":2053,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-generate-slug-from-string","Генерация slug из строки","Преобразует произвольную строку (в том числе с кириллицей) в URL-friendly slug — строчные буквы, без пробелов и спецсимволов, дефисы вместо пробелов.",[1612,1440,1670],"2026-04-30",{"path":2055,"title":2056,"description":2057,"language":1612,"tags":2058,"date":2059,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-format-date-in-timezone","Форматирование даты\u002Fвремени в любом IANA-часовом поясе","Отображает дату так, будто вы находитесь в конкретном часовом поясе, независимо от локального времени посетителя.",[1612,1727,1753],"2026-04-29",{"path":2061,"title":2062,"description":2063,"language":1612,"tags":2064,"date":2065,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-format-compact-number","Компактное форматирование чисел (1.2K, 3M)","Превращает большие числа в короткие читаемые обозначения — 600000 становится 600K.",[1612,1727],"2026-04-28",{"path":2067,"title":2068,"description":2069,"language":1612,"tags":2070,"date":2071,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-detect-visitor-timezone","Автоматическое определение часового пояса посетителя","Одна строка, чтобы получить IANA-часовой пояс посетителя (например, \"Asia\u002FJerusalem\") прямо из браузера, без запроса геолокации.",[1612,1727,1753],"2026-04-27",{"path":2073,"title":2074,"description":2075,"language":1612,"tags":2076,"date":2077,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-deep-clone-object","Глубокое клонирование объекта","Создаёт полную независимую копию объекта без ссылок — использует нативный structuredClone там, где он доступен, и падает обратно на JSON-round-trip для старых сред.",[1612,1440,1670],"2026-04-26",{"path":2079,"title":2080,"description":2081,"language":1612,"tags":2082,"date":2083,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-dedupe-sort-tag-list","Управление уникальным отсортированным списком тегов","Операции добавления\u002Fудаления для массива тегов, который всегда остаётся уникальным и отсортированным по алфавиту — удобно для тег-пикеров и фильтров-чипов.",[1612,1727],"2026-04-25",{"path":2085,"title":2086,"description":2087,"language":1612,"tags":2088,"date":2089,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-debounce-function","Дебаунс функции","Откладывает выполнение функции до тех пор, пока серия вызовов не прекратится — идеально для полей поиска и обработчиков resize.",[1612,1727],"2026-04-24",{"path":2091,"title":2092,"description":2093,"language":1612,"tags":2094,"date":2096,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-cosine-similarity","Косинусное сходство между двумя векторами","Измеряет, насколько похожи два вектора эмбеддинга — базовая операция для семантического поиска и RAG-извлечения.",[1612,2095,1727],"AI","2026-04-23",{"path":2098,"title":2099,"description":2100,"language":1612,"tags":2101,"date":2102,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-cn-classname-merge","Безопасное объединение Tailwind-классов (хелпер cn)","Комбинирует условные классы через clsx и разрешает конфликты Tailwind-утилит через tailwind-merge — стандартный хелпер \"cn()\", используемый в кодовых базах в стиле shadcn\u002Fui.",[1612,1532,1727],"2026-04-22",{"path":2104,"title":2105,"description":2106,"language":1612,"tags":2107,"date":2108,"lang":1356},"\u002Fsnippets\u002Fru\u002Fjavascript-capitalize-first-letter","Заглавная первая буква строки","Короткий и правильный способ сделать заглавной только первую букву — плюс вариант, который делает заглавной первую букву каждого слова.",[1612,1727],"2026-04-21",{"path":2110,"title":1878,"description":2111,"language":2112,"tags":2113,"date":2114,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgo-simple-http-server","Способ обслуживать HTTP-роуты средствами стандартной библиотеки Go — фреймворк не нужен для небольшого API.","Go",[2112,1582],"2026-04-20",{"path":2116,"title":2117,"description":2118,"language":2112,"tags":2119,"date":2120,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgo-read-file-lines-go","Построчное чтение файла","Потоковое чтение текстового файла построчно без загрузки всего файла в память.",[2112,1720],"2026-04-19",{"path":2122,"title":2123,"description":2124,"language":2112,"tags":2125,"date":2126,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgo-http-get-request","HTTP GET-запрос","Способ получить и разобрать JSON от API средствами стандартной библиотеки — без стороннего HTTP-клиента.",[2112,1582],"2026-04-18",{"path":2128,"title":2129,"description":2130,"language":2112,"tags":2131,"date":2136,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgo-graceful-shutdown","HTTP Server с Graceful Shutdown (Go)","Шаблон HTTP-сервера на Go с корректным завершением работы при получении сигналов SIGINT\u002FSIGTERM. Завершает активные соединения и закрывает БД.",[2112,2132,2133,2134,2135],"Golang","HTTP","Backend","Concurrency","2026-04-17",{"path":2138,"title":2139,"description":2140,"language":2112,"tags":2141,"date":2142,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgo-goroutines-waitgroup","Параллельное выполнение задач через горутины","Стандартный паттерн запуска нескольких горутин и ожидания завершения всех через sync.WaitGroup.",[2112,1727],"2026-04-16",{"path":2144,"title":2044,"description":2145,"language":2112,"tags":2146,"date":2147,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgo-generate-uuid","Создаёт случайный UUID v4, используя только crypto\u002Frand из стандартной библиотеки — без внешнего пакета uuid.",[2112,1727],"2026-04-15",{"path":2149,"title":2150,"description":2151,"language":2112,"tags":2152,"date":2154,"lang":1356},"\u002Fsnippets\u002Fru\u002Fgo-context-timeout","HTTP запрос с таймаутом через Context (Go)","Создание HTTP-запроса на Go с использованием context.WithTimeout. Гарантирует прерывание сетевых запросов при превышении заданного времени.",[2112,2132,2133,2153,1376],"Timeout","2026-04-14",{"path":2156,"title":2157,"description":2158,"language":1571,"tags":2159,"date":2160,"lang":1356},"\u002Fsnippets\u002Fru\u002Fexpress.js-sanitize-filename-express","Санитизация имени загруженного файла","Убирает расширение файла и приводит оставшееся имя к безопасному URL-совместимому виду перед сохранением на диск.",[1571,1365,1612],"2026-04-13",{"path":2162,"title":2163,"description":2164,"language":1571,"tags":2165,"date":2166,"lang":1356},"\u002Fsnippets\u002Fru\u002Fexpress.js-rate-limiting-middleware","Ограничение частоты запросов","Ограничивает, сколько запросов может сделать один IP за временное окно — стандартная первая линия защиты от абьюза и брутфорса.",[1571,1365,1518],"2026-04-12",{"path":2168,"title":2169,"description":2170,"language":1571,"tags":2171,"date":2172,"lang":1356},"\u002Fsnippets\u002Fru\u002Fexpress.js-jwt-auth-middleware","JWT Auth Middleware (Express)","Express middleware для проверки авторизации через JWT. Считывает заголовок Authorization, верифицирует токен и безопасно расширяет Request объект в TypeScript.",[1571,1365,1518,1573,1440],"2026-04-11",{"path":2174,"title":2175,"description":2176,"language":1571,"tags":2177,"date":2178,"lang":1356},"\u002Fsnippets\u002Fru\u002Fexpress.js-error-handling-middleware","Централизованный middleware обработки ошибок","Ловит ошибки из любого роута\u002Fmiddleware в одном месте, вместо повторения try\u002Fcatch и логики форматирования ошибок повсюду.",[1571,1365],"2026-04-10",{"path":2180,"title":2181,"description":2182,"language":1571,"tags":2183,"date":2184,"lang":1356},"\u002Fsnippets\u002Fru\u002Fexpress.js-cors-middleware","Включение CORS в Express","Стандартный способ разрешить кросс-доменные запросы с фронтенда — либо быстро и открыто для всех, либо ограничено конкретными источниками.",[1571,1365,1582],"2026-04-09",{"path":2186,"title":2187,"description":2188,"language":1532,"tags":2189,"date":2190,"lang":1356},"\u002Fsnippets\u002Fru\u002Fcss-truncate-text-ellipsis","Обрезание однострочного текста с многоточием","Обрезает текст, не помещающийся в одну строку, и добавляет \"…\" в конце — классический способ обрезки текста только средствами CSS.",[1532,1727],"2026-04-08",{"path":2192,"title":2193,"description":2194,"language":1532,"tags":2195,"date":2197,"lang":1356},"\u002Fsnippets\u002Fru\u002Fcss-skeleton-loading","Skeleton Loading анимация (CSS)","Shimmer-эффект для скелетонов загрузки — пульсирующий градиент имитирует загрузку контента. Без JavaScript, только CSS-анимация на кастомных свойствах.",[1532,2196,1395],"UI","2026-04-07",{"path":2199,"title":2200,"description":2201,"language":1532,"tags":2202,"date":2204,"lang":1356},"\u002Fsnippets\u002Fru\u002Fcss-line-clamp","Многострочный text-overflow с ellipsis","Обрезает текст после N строк с многоточием через -webkit-line-clamp — работает во всех современных браузерах без JavaScript.",[1532,1670,2203],"Typography","2026-04-06",{"path":2206,"title":2207,"description":2208,"language":1532,"tags":2209,"date":2211,"lang":1356},"\u002Fsnippets\u002Fru\u002Fcss-grid-auto-fit","CSS Grid auto-fit — адаптивная сетка без media queries","Создаёт автоматически адаптируемую сетку колонок через auto-fit и minmax — карточки сами перестраиваются при изменении ширины контейнера без единого breakpoint.",[1532,2210,1386,1533],"Layout","2026-04-05",{"path":2213,"title":2214,"description":2215,"language":1532,"tags":2216,"date":2217,"lang":1356},"\u002Fsnippets\u002Fru\u002Fcss-gradient-text","Градиентный текст (CSS)","Применяет линейный или радиальный градиент к тексту через background-clip: text — без SVG, без JavaScript. Поддерживается всеми современными браузерами.",[1532,2196,2203],"2026-04-04",{"path":2219,"title":2220,"description":2221,"language":1532,"tags":2222,"date":2223,"lang":1356},"\u002Fsnippets\u002Fru\u002Fcss-fluid-typography","Fluid Typography с clamp()","Масштабирует размер шрифта плавно между минимальным и максимальным значением в зависимости от ширины viewport — без media queries, только clamp().",[1532,2203,1386],"2026-04-03",{"path":2225,"title":2226,"description":2227,"language":1532,"tags":2228,"date":2229,"lang":1356},"\u002Fsnippets\u002Fru\u002Fcss-custom-scrollbar","Кастомный scrollbar через CSS","Стилизует полосу прокрутки под дизайн-систему без JavaScript — тонкий трек, скруглённый ползунок, hover-состояние. Включает вариант для скрытия scrollbar с сохранением прокрутки.",[1532,1670,2196],"2026-04-02",{"path":2231,"title":2232,"description":2233,"language":1532,"tags":2234,"date":2235,"lang":1356},"\u002Fsnippets\u002Fru\u002Fcss-center-with-flexbox-css","Центрирование чего угодно через flexbox","Переиспользуемый класс, который идеально центрирует единственного потомка по горизонтали и вертикали.",[1532,2210],"2026-04-01",{"id":2237,"title":1592,"body":2238,"date":1597,"description":1593,"extension":1344,"lang":1356,"language":1446,"meta":2488,"navigation":106,"path":1591,"published":106,"seo":2489,"stem":2490,"tags":2491,"__hash__":2492},"snippets\u002Fsnippets\u002Fru\u002Fnginx-reverse-proxy-ssl.md",{"type":8,"value":2239,"toc":2486},[2240,2243,2439,2483],[11,2241,2242],{},"Как это работает: nginx завершает TLS на границе, перенаправляет запросы на локальный Node.js бэкенд, добавляет заголовки безопасности, ограничение скорости и gzip-сжатие — до того, как запрос дойдёт до приложения.",[34,2244,2248],{"className":2245,"code":2246,"language":2247,"meta":40,"style":40},"language-nginx shiki shiki-themes github-dark","server {\n    listen 443 ssl http2;\n    server_name yourdomain.com;\n\n    ssl_certificate     \u002Fetc\u002Fletsencrypt\u002Flive\u002Fyourdomain.com\u002Ffullchain.pem;\n    ssl_certificate_key \u002Fetc\u002Fletsencrypt\u002Flive\u002Fyourdomain.com\u002Fprivkey.pem;\n    ssl_protocols       TLSv1.2 TLSv1.3;\n    ssl_ciphers         HIGH:!aNULL:!MD5;\n\n    # Заголовки безопасности\n    add_header Strict-Transport-Security \"max-age=63072000\" always;\n    add_header X-Frame-Options \"SAMEORIGIN\" always;\n    add_header X-Content-Type-Options \"nosniff\" always;\n    add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\n\n    # Rate limiting\n    limit_req zone=api burst=20 nodelay;\n    limit_req_status 429;\n\n    # Gzip\n    gzip on;\n    gzip_types text\u002Fplain text\u002Fcss application\u002Fjson application\u002Fjavascript;\n\n    location \u002F {\n        proxy_pass http:\u002F\u002F127.0.0.1:3000;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection \"upgrade\";\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n}\n\nserver {\n    listen 80;\n    server_name yourdomain.com;\n    return 301 https:\u002F\u002F$host$request_uri;\n}\n","nginx",[42,2249,2250,2255,2260,2265,2269,2274,2279,2284,2289,2293,2298,2303,2308,2313,2318,2322,2327,2332,2337,2341,2346,2351,2356,2360,2365,2370,2375,2380,2385,2390,2395,2400,2405,2409,2413,2417,2421,2426,2430,2435],{"__ignoreMap":40},[79,2251,2252],{"class":81,"line":82},[79,2253,2254],{},"server {\n",[79,2256,2257],{"class":81,"line":103},[79,2258,2259],{},"    listen 443 ssl http2;\n",[79,2261,2262],{"class":81,"line":110},[79,2263,2264],{},"    server_name yourdomain.com;\n",[79,2266,2267],{"class":81,"line":123},[79,2268,107],{"emptyLinePlaceholder":106},[79,2270,2271],{"class":81,"line":139},[79,2272,2273],{},"    ssl_certificate     \u002Fetc\u002Fletsencrypt\u002Flive\u002Fyourdomain.com\u002Ffullchain.pem;\n",[79,2275,2276],{"class":81,"line":152},[79,2277,2278],{},"    ssl_certificate_key \u002Fetc\u002Fletsencrypt\u002Flive\u002Fyourdomain.com\u002Fprivkey.pem;\n",[79,2280,2281],{"class":81,"line":165},[79,2282,2283],{},"    ssl_protocols       TLSv1.2 TLSv1.3;\n",[79,2285,2286],{"class":81,"line":177},[79,2287,2288],{},"    ssl_ciphers         HIGH:!aNULL:!MD5;\n",[79,2290,2291],{"class":81,"line":189},[79,2292,107],{"emptyLinePlaceholder":106},[79,2294,2295],{"class":81,"line":202},[79,2296,2297],{},"    # Заголовки безопасности\n",[79,2299,2300],{"class":81,"line":208},[79,2301,2302],{},"    add_header Strict-Transport-Security \"max-age=63072000\" always;\n",[79,2304,2305],{"class":81,"line":213},[79,2306,2307],{},"    add_header X-Frame-Options \"SAMEORIGIN\" always;\n",[79,2309,2310],{"class":81,"line":223},[79,2311,2312],{},"    add_header X-Content-Type-Options \"nosniff\" always;\n",[79,2314,2315],{"class":81,"line":235},[79,2316,2317],{},"    add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\n",[79,2319,2320],{"class":81,"line":247},[79,2321,107],{"emptyLinePlaceholder":106},[79,2323,2324],{"class":81,"line":252},[79,2325,2326],{},"    # Rate limiting\n",[79,2328,2329],{"class":81,"line":257},[79,2330,2331],{},"    limit_req zone=api burst=20 nodelay;\n",[79,2333,2334],{"class":81,"line":298},[79,2335,2336],{},"    limit_req_status 429;\n",[79,2338,2339],{"class":81,"line":319},[79,2340,107],{"emptyLinePlaceholder":106},[79,2342,2343],{"class":81,"line":343},[79,2344,2345],{},"    # Gzip\n",[79,2347,2348],{"class":81,"line":349},[79,2349,2350],{},"    gzip on;\n",[79,2352,2353],{"class":81,"line":364},[79,2354,2355],{},"    gzip_types text\u002Fplain text\u002Fcss application\u002Fjson application\u002Fjavascript;\n",[79,2357,2358],{"class":81,"line":379},[79,2359,107],{"emptyLinePlaceholder":106},[79,2361,2362],{"class":81,"line":385},[79,2363,2364],{},"    location \u002F {\n",[79,2366,2367],{"class":81,"line":390},[79,2368,2369],{},"        proxy_pass http:\u002F\u002F127.0.0.1:3000;\n",[79,2371,2372],{"class":81,"line":397},[79,2373,2374],{},"        proxy_http_version 1.1;\n",[79,2376,2377],{"class":81,"line":450},[79,2378,2379],{},"        proxy_set_header Upgrade $http_upgrade;\n",[79,2381,2382],{"class":81,"line":455},[79,2383,2384],{},"        proxy_set_header Connection \"upgrade\";\n",[79,2386,2387],{"class":81,"line":461},[79,2388,2389],{},"        proxy_set_header Host $host;\n",[79,2391,2392],{"class":81,"line":474},[79,2393,2394],{},"        proxy_set_header X-Real-IP $remote_addr;\n",[79,2396,2397],{"class":81,"line":516},[79,2398,2399],{},"        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n",[79,2401,2402],{"class":81,"line":536},[79,2403,2404],{},"        proxy_set_header X-Forwarded-Proto $scheme;\n",[79,2406,2407],{"class":81,"line":541},[79,2408,1190],{},[79,2410,2411],{"class":81,"line":547},[79,2412,205],{},[79,2414,2415],{"class":81,"line":553},[79,2416,107],{"emptyLinePlaceholder":106},[79,2418,2419],{"class":81,"line":566},[79,2420,2254],{},[79,2422,2423],{"class":81,"line":586},[79,2424,2425],{},"    listen 80;\n",[79,2427,2428],{"class":81,"line":597},[79,2429,2264],{},[79,2431,2432],{"class":81,"line":607},[79,2433,2434],{},"    return 301 https:\u002F\u002F$host$request_uri;\n",[79,2436,2437],{"class":81,"line":612},[79,2438,205],{},[34,2440,2444],{"className":2441,"code":2442,"language":2443,"meta":40,"style":40},"language-bash shiki shiki-themes github-dark","# Проверить конфиг перед перезагрузкой\nsudo nginx -t\n\n# Перезагрузить без разрыва соединений\nsudo nginx -s reload\n","bash",[42,2445,2446,2451,2462,2466,2471],{"__ignoreMap":40},[79,2447,2448],{"class":81,"line":82},[79,2449,2450],{"class":393},"# Проверить конфиг перед перезагрузкой\n",[79,2452,2453,2456,2459],{"class":81,"line":103},[79,2454,2455],{"class":116},"sudo",[79,2457,2458],{"class":96}," nginx",[79,2460,2461],{"class":133}," -t\n",[79,2463,2464],{"class":81,"line":110},[79,2465,107],{"emptyLinePlaceholder":106},[79,2467,2468],{"class":81,"line":123},[79,2469,2470],{"class":393},"# Перезагрузить без разрыва соединений\n",[79,2472,2473,2475,2477,2480],{"class":81,"line":139},[79,2474,2455],{"class":116},[79,2476,2458],{"class":96},[79,2478,2479],{"class":133}," -s",[79,2481,2482],{"class":96}," reload\n",[1328,2484,2485],{},"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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}",{"title":40,"searchDepth":103,"depth":103,"links":2487},[],{},{"title":1592,"description":1593},"snippets\u002Fru\u002Fnginx-reverse-proxy-ssl",[1446,1458,1595,1596],"16F-b2MIPTbDAazEwIB2UNvUe3qkkdCkn3jKA9vEMnY",1784561417362]