[{"data":1,"prerenderedAt":2606},["ShallowReactive",2],{"header-latest":3,"snippets-list":1372,"snippet-detail-php-get-client-ip-behind-proxy-en":2242},[4],{"id":5,"title":6,"body":7,"date":1346,"description":1347,"extension":1348,"faq":1349,"image":1359,"lang":1360,"meta":1361,"navigation":108,"path":1362,"published":108,"readTime":215,"seo":1363,"stem":1364,"tags":1365,"updated":1346,"__hash__":1371},"blog\u002Fblog\u002Fen\u002Ftelegram-web-apps-sells.md","Telegram Web Apps (TWA): How to Launch a Full SaaS Inside Telegram",{"type":8,"value":9,"toc":1335},"minimark",[10,19,22,25,30,33,44,52,63,70,73,856,860,863,868,875,923,927,930,1215,1219,1229,1232,1270,1283,1291,1295,1315,1318,1331],[11,12,13,14,18],"p",{},"Telegram has transcended its origins as a privacy-focused messaging client to become a powerful, cross-platform runtime environment. The introduction of ",[15,16,17],"strong",{},"Telegram Web Apps (TWA)",", officially known as Telegram Mini Apps, allows developers to build highly interactive single-page applications (SPAs) that load directly inside the Telegram application shell.",[11,20,21],{},"This provides startups and enterprises with immediate, frictionless access to over 900 million active users. By embedding your B2B SaaS, CRM interface, or utility tool inside a Telegram bot, you remove the classic conversion hurdles of app store downloads, desktop logins, and sign-up flows.",[11,23,24],{},"In this developer guide, we will analyze the technical architecture of a TWA, implement a secure hash verification check on the backend, and look at frontend integration patterns.",[26,27,29],"h2",{"id":28},"the-telegram-mini-app-ecosystem-architecture","The Telegram Mini App Ecosystem Architecture",[11,31,32],{},"Unlike traditional web applications, a TWA has a dual parent relationship. The frontend runs in a sandboxed browser component (WebView) controlled by the Telegram client app, while the backend speaks both to the Telegram Bot API and your application database.",[34,35,41],"pre",{"className":36,"code":38,"language":39,"meta":40},[37],"language-text","+-----------------------------------------------------------+\n|                      Telegram Client                      |\n|  +-----------------------------------------------------+  |\n|  |                Mini App (WebView UI)                |  |\n|  |     (Uses window.Telegram.WebApp SDK for bridge)    |  |\n|  +--------------------------+--------------------------+  |\n+-----------------------------|-----------------------------+\n       | (initData Query)     | (Secure HTTPS Request)\n       v                      v\n+--------------+       +------------------------------------+\n| Telegram API |       |            Your Backend            |\n|  (Webhooks)  |       |  (Validates initData via HMAC-256) |\n+------+-------+       +-----------------+------------------+\n       |                                 |\n       +----------------> [DB Sync] \u003C----+\n","text","",[42,43,38],"code",{"__ignoreMap":40},[26,45,47,48,51],{"id":46},"_1-security-first-validating-the-initdata-payload","1. Security First: Validating the ",[42,49,50],{},"initData"," Payload",[11,53,54,55,58,59,62],{},"When a user opens your Mini App, Telegram appends a parameter called ",[42,56,57],{},"tgWebAppData"," (or raw search parameters) containing user profiles, launch contexts, and a security ",[42,60,61],{},"hash",".",[11,64,65,66,69],{},"To prevent users from modifying their user IDs or mocking paid subscription privileges, you ",[15,67,68],{},"must validate this hash on your backend"," using your Telegram Bot Token as the HMAC key.",[11,71,72],{},"Here is the cryptographic validation implementation using Node.js:",[34,74,78],{"className":75,"code":76,"language":77,"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. Sort all incoming parameters alphabetically, excluding the hash itself\n  const keys = Array.from(urlParams.keys()).filter(key => key !== 'hash').sort();\n  \n  \u002F\u002F 2. Re-create the verification data-check string\n  const dataCheckString = keys\n    .map(key => `${key}=${urlParams.get(key)}`)\n    .join('\\n');\n\n  \u002F\u002F 3. Generate the secret cryptographic key\n  \u002F\u002F We use the constant string \"WebAppData\" to sign the bot token first\n  const secretKey = crypto\n    .createHmac('sha256', 'WebAppData')\n    .update(botToken)\n    .digest();\n\n  \u002F\u002F 4. Calculate the expected hash of the sorted string\n  const computedHash = crypto\n    .createHmac('sha256', secretKey)\n    .update(dataCheckString)\n    .digest('hex');\n\n  \u002F\u002F 5. Compare computed signature with the signature sent by the client\n  const isValid = computedHash === hash;\n\n  if (!isValid) {\n    return { isValid: false };\n  }\n\n  \u002F\u002F Parse user data object if validation succeeded\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,79,80,103,110,123,139,152,165,177,189,202,208,213,223,235,247,252,257,298,319,343,349,364,379,385,390,397,450,455,461,474,516,536,541,547,553,566,586,597,607,612,618,630,644,654,668,673,679,698,703,715,726,731,736,742,750,772,810,823,835,846,851],{"__ignoreMap":40},[81,82,85,89,93,96,100],"span",{"class":83,"line":84},"line",1,[81,86,88],{"class":87},"snl16","import",[81,90,92],{"class":91},"s95oV"," crypto ",[81,94,95],{"class":87},"from",[81,97,99],{"class":98},"sU2Wk"," 'crypto'",[81,101,102],{"class":91},";\n",[81,104,106],{"class":83,"line":105},2,[81,107,109],{"emptyLinePlaceholder":108},true,"\n",[81,111,113,116,120],{"class":83,"line":112},3,[81,114,115],{"class":87},"interface",[81,117,119],{"class":118},"svObZ"," TelegramUserData",[81,121,122],{"class":91}," {\n",[81,124,126,130,133,137],{"class":83,"line":125},4,[81,127,129],{"class":128},"s9osk","  id",[81,131,132],{"class":87},":",[81,134,136],{"class":135},"sDLfK"," number",[81,138,102],{"class":91},[81,140,142,145,147,150],{"class":83,"line":141},5,[81,143,144],{"class":128},"  first_name",[81,146,132],{"class":87},[81,148,149],{"class":135}," string",[81,151,102],{"class":91},[81,153,155,158,161,163],{"class":83,"line":154},6,[81,156,157],{"class":128},"  last_name",[81,159,160],{"class":87},"?:",[81,162,149],{"class":135},[81,164,102],{"class":91},[81,166,168,171,173,175],{"class":83,"line":167},7,[81,169,170],{"class":128},"  username",[81,172,160],{"class":87},[81,174,149],{"class":135},[81,176,102],{"class":91},[81,178,180,183,185,187],{"class":83,"line":179},8,[81,181,182],{"class":128},"  language_code",[81,184,160],{"class":87},[81,186,149],{"class":135},[81,188,102],{"class":91},[81,190,192,195,197,200],{"class":83,"line":191},9,[81,193,194],{"class":128},"  is_premium",[81,196,160],{"class":87},[81,198,199],{"class":135}," boolean",[81,201,102],{"class":91},[81,203,205],{"class":83,"line":204},10,[81,206,207],{"class":91},"}\n",[81,209,211],{"class":83,"line":210},11,[81,212,109],{"emptyLinePlaceholder":108},[81,214,216,218,221],{"class":83,"line":215},12,[81,217,115],{"class":87},[81,219,220],{"class":118}," ValidationResult",[81,222,122],{"class":91},[81,224,226,229,231,233],{"class":83,"line":225},13,[81,227,228],{"class":128},"  isValid",[81,230,132],{"class":87},[81,232,199],{"class":135},[81,234,102],{"class":91},[81,236,238,241,243,245],{"class":83,"line":237},14,[81,239,240],{"class":128},"  user",[81,242,160],{"class":87},[81,244,119],{"class":118},[81,246,102],{"class":91},[81,248,250],{"class":83,"line":249},15,[81,251,207],{"class":91},[81,253,255],{"class":83,"line":254},16,[81,256,109],{"emptyLinePlaceholder":108},[81,258,260,263,266,269,272,275,277,279,282,285,287,289,292,294,296],{"class":83,"line":259},17,[81,261,262],{"class":87},"export",[81,264,265],{"class":87}," function",[81,267,268],{"class":118}," verifyTelegramInitData",[81,270,271],{"class":91},"(",[81,273,274],{"class":128},"rawQueryString",[81,276,132],{"class":87},[81,278,149],{"class":135},[81,280,281],{"class":91},", ",[81,283,284],{"class":128},"botToken",[81,286,132],{"class":87},[81,288,149],{"class":135},[81,290,291],{"class":91},")",[81,293,132],{"class":87},[81,295,220],{"class":118},[81,297,122],{"class":91},[81,299,301,304,307,310,313,316],{"class":83,"line":300},18,[81,302,303],{"class":87},"  const",[81,305,306],{"class":135}," urlParams",[81,308,309],{"class":87}," =",[81,311,312],{"class":87}," new",[81,314,315],{"class":118}," URLSearchParams",[81,317,318],{"class":91},"(rawQueryString);\n",[81,320,322,324,327,329,332,335,337,340],{"class":83,"line":321},19,[81,323,303],{"class":87},[81,325,326],{"class":135}," hash",[81,328,309],{"class":87},[81,330,331],{"class":91}," urlParams.",[81,333,334],{"class":118},"get",[81,336,271],{"class":91},[81,338,339],{"class":98},"'hash'",[81,341,342],{"class":91},");\n",[81,344,346],{"class":83,"line":345},20,[81,347,348],{"class":91},"  \n",[81,350,352,355,358,361],{"class":83,"line":351},21,[81,353,354],{"class":87},"  if",[81,356,357],{"class":91}," (",[81,359,360],{"class":87},"!",[81,362,363],{"class":91},"hash) {\n",[81,365,367,370,373,376],{"class":83,"line":366},22,[81,368,369],{"class":87},"    return",[81,371,372],{"class":91}," { isValid: ",[81,374,375],{"class":135},"false",[81,377,378],{"class":91}," };\n",[81,380,382],{"class":83,"line":381},23,[81,383,384],{"class":91},"  }\n",[81,386,388],{"class":83,"line":387},24,[81,389,109],{"emptyLinePlaceholder":108},[81,391,393],{"class":83,"line":392},25,[81,394,396],{"class":395},"sAwPA","  \u002F\u002F 1. Sort all incoming parameters alphabetically, excluding the hash itself\n",[81,398,400,402,405,407,410,412,415,418,421,424,426,429,432,435,438,441,444,447],{"class":83,"line":399},26,[81,401,303],{"class":87},[81,403,404],{"class":135}," keys",[81,406,309],{"class":87},[81,408,409],{"class":91}," Array.",[81,411,95],{"class":118},[81,413,414],{"class":91},"(urlParams.",[81,416,417],{"class":118},"keys",[81,419,420],{"class":91},"()).",[81,422,423],{"class":118},"filter",[81,425,271],{"class":91},[81,427,428],{"class":128},"key",[81,430,431],{"class":87}," =>",[81,433,434],{"class":91}," key ",[81,436,437],{"class":87},"!==",[81,439,440],{"class":98}," 'hash'",[81,442,443],{"class":91},").",[81,445,446],{"class":118},"sort",[81,448,449],{"class":91},"();\n",[81,451,453],{"class":83,"line":452},27,[81,454,348],{"class":91},[81,456,458],{"class":83,"line":457},28,[81,459,460],{"class":395},"  \u002F\u002F 2. Re-create the verification data-check string\n",[81,462,464,466,469,471],{"class":83,"line":463},29,[81,465,303],{"class":87},[81,467,468],{"class":135}," dataCheckString",[81,470,309],{"class":87},[81,472,473],{"class":91}," keys\n",[81,475,477,480,483,485,487,489,492,494,497,500,502,504,506,508,510,513],{"class":83,"line":476},30,[81,478,479],{"class":91},"    .",[81,481,482],{"class":118},"map",[81,484,271],{"class":91},[81,486,428],{"class":128},[81,488,431],{"class":87},[81,490,491],{"class":98}," `${",[81,493,428],{"class":91},[81,495,496],{"class":98},"}=${",[81,498,499],{"class":91},"urlParams",[81,501,62],{"class":98},[81,503,334],{"class":118},[81,505,271],{"class":98},[81,507,428],{"class":91},[81,509,291],{"class":98},[81,511,512],{"class":98},"}`",[81,514,515],{"class":91},")\n",[81,517,519,521,524,526,529,532,534],{"class":83,"line":518},31,[81,520,479],{"class":91},[81,522,523],{"class":118},"join",[81,525,271],{"class":91},[81,527,528],{"class":98},"'",[81,530,531],{"class":135},"\\n",[81,533,528],{"class":98},[81,535,342],{"class":91},[81,537,539],{"class":83,"line":538},32,[81,540,109],{"emptyLinePlaceholder":108},[81,542,544],{"class":83,"line":543},33,[81,545,546],{"class":395},"  \u002F\u002F 3. Generate the secret cryptographic key\n",[81,548,550],{"class":83,"line":549},34,[81,551,552],{"class":395},"  \u002F\u002F We use the constant string \"WebAppData\" to sign the bot token first\n",[81,554,556,558,561,563],{"class":83,"line":555},35,[81,557,303],{"class":87},[81,559,560],{"class":135}," secretKey",[81,562,309],{"class":87},[81,564,565],{"class":91}," crypto\n",[81,567,569,571,574,576,579,581,584],{"class":83,"line":568},36,[81,570,479],{"class":91},[81,572,573],{"class":118},"createHmac",[81,575,271],{"class":91},[81,577,578],{"class":98},"'sha256'",[81,580,281],{"class":91},[81,582,583],{"class":98},"'WebAppData'",[81,585,515],{"class":91},[81,587,589,591,594],{"class":83,"line":588},37,[81,590,479],{"class":91},[81,592,593],{"class":118},"update",[81,595,596],{"class":91},"(botToken)\n",[81,598,600,602,605],{"class":83,"line":599},38,[81,601,479],{"class":91},[81,603,604],{"class":118},"digest",[81,606,449],{"class":91},[81,608,610],{"class":83,"line":609},39,[81,611,109],{"emptyLinePlaceholder":108},[81,613,615],{"class":83,"line":614},40,[81,616,617],{"class":395},"  \u002F\u002F 4. Calculate the expected hash of the sorted string\n",[81,619,621,623,626,628],{"class":83,"line":620},41,[81,622,303],{"class":87},[81,624,625],{"class":135}," computedHash",[81,627,309],{"class":87},[81,629,565],{"class":91},[81,631,633,635,637,639,641],{"class":83,"line":632},42,[81,634,479],{"class":91},[81,636,573],{"class":118},[81,638,271],{"class":91},[81,640,578],{"class":98},[81,642,643],{"class":91},", secretKey)\n",[81,645,647,649,651],{"class":83,"line":646},43,[81,648,479],{"class":91},[81,650,593],{"class":118},[81,652,653],{"class":91},"(dataCheckString)\n",[81,655,657,659,661,663,666],{"class":83,"line":656},44,[81,658,479],{"class":91},[81,660,604],{"class":118},[81,662,271],{"class":91},[81,664,665],{"class":98},"'hex'",[81,667,342],{"class":91},[81,669,671],{"class":83,"line":670},45,[81,672,109],{"emptyLinePlaceholder":108},[81,674,676],{"class":83,"line":675},46,[81,677,678],{"class":395},"  \u002F\u002F 5. Compare computed signature with the signature sent by the client\n",[81,680,682,684,687,689,692,695],{"class":83,"line":681},47,[81,683,303],{"class":87},[81,685,686],{"class":135}," isValid",[81,688,309],{"class":87},[81,690,691],{"class":91}," computedHash ",[81,693,694],{"class":87},"===",[81,696,697],{"class":91}," hash;\n",[81,699,701],{"class":83,"line":700},48,[81,702,109],{"emptyLinePlaceholder":108},[81,704,706,708,710,712],{"class":83,"line":705},49,[81,707,354],{"class":87},[81,709,357],{"class":91},[81,711,360],{"class":87},[81,713,714],{"class":91},"isValid) {\n",[81,716,718,720,722,724],{"class":83,"line":717},50,[81,719,369],{"class":87},[81,721,372],{"class":91},[81,723,375],{"class":135},[81,725,378],{"class":91},[81,727,729],{"class":83,"line":728},51,[81,730,384],{"class":91},[81,732,734],{"class":83,"line":733},52,[81,735,109],{"emptyLinePlaceholder":108},[81,737,739],{"class":83,"line":738},53,[81,740,741],{"class":395},"  \u002F\u002F Parse user data object if validation succeeded\n",[81,743,745,748],{"class":83,"line":744},54,[81,746,747],{"class":87},"  try",[81,749,122],{"class":91},[81,751,753,756,759,761,763,765,767,770],{"class":83,"line":752},55,[81,754,755],{"class":87},"    const",[81,757,758],{"class":135}," userRaw",[81,760,309],{"class":87},[81,762,331],{"class":91},[81,764,334],{"class":118},[81,766,271],{"class":91},[81,768,769],{"class":98},"'user'",[81,771,342],{"class":91},[81,773,775,777,780,782,784,786,789,792,795,797,800,803,805,808],{"class":83,"line":774},56,[81,776,755],{"class":87},[81,778,779],{"class":135}," user",[81,781,132],{"class":87},[81,783,119],{"class":118},[81,785,309],{"class":87},[81,787,788],{"class":91}," userRaw ",[81,790,791],{"class":87},"?",[81,793,794],{"class":135}," JSON",[81,796,62],{"class":91},[81,798,799],{"class":118},"parse",[81,801,802],{"class":91},"(userRaw) ",[81,804,132],{"class":87},[81,806,807],{"class":135}," undefined",[81,809,102],{"class":91},[81,811,813,815,817,820],{"class":83,"line":812},57,[81,814,369],{"class":87},[81,816,372],{"class":91},[81,818,819],{"class":135},"true",[81,821,822],{"class":91},", user };\n",[81,824,826,829,832],{"class":83,"line":825},58,[81,827,828],{"class":91},"  } ",[81,830,831],{"class":87},"catch",[81,833,834],{"class":91}," (error) {\n",[81,836,838,840,842,844],{"class":83,"line":837},59,[81,839,369],{"class":87},[81,841,372],{"class":91},[81,843,819],{"class":135},[81,845,378],{"class":91},[81,847,849],{"class":83,"line":848},60,[81,850,384],{"class":91},[81,852,854],{"class":83,"line":853},61,[81,855,207],{"class":91},[26,857,859],{"id":858},"_2-frontend-integration-theme-synchronization","2. Frontend Integration & Theme Synchronization",[11,861,862],{},"To deliver a premium UI\u002FUX, your Mini App should visually blend with the Telegram client’s dark\u002Flight settings. You can access the styles and control client-side behaviors using the official Telegram WebApp JS library.",[864,865,867],"h3",{"id":866},"step-1-include-the-script-in-nuxt-3-html","Step 1: Include the Script in Nuxt 3 \u002F HTML",[11,869,870,871,874],{},"Add the official script to your page header or use the ",[42,872,873],{},"useHead"," composable in Nuxt:",[34,876,878],{"className":75,"code":877,"language":77,"meta":40,"style":40},"\u002F\u002F app.vue or page layout\nuseHead({\n  script: [\n    { src: 'https:\u002F\u002Ftelegram.org\u002Fjs\u002Ftelegram-web-app.js', defer: true }\n  ]\n})\n",[42,879,880,885,892,897,913,918],{"__ignoreMap":40},[81,881,882],{"class":83,"line":84},[81,883,884],{"class":395},"\u002F\u002F app.vue or page layout\n",[81,886,887,889],{"class":83,"line":105},[81,888,873],{"class":118},[81,890,891],{"class":91},"({\n",[81,893,894],{"class":83,"line":112},[81,895,896],{"class":91},"  script: [\n",[81,898,899,902,905,908,910],{"class":83,"line":125},[81,900,901],{"class":91},"    { src: ",[81,903,904],{"class":98},"'https:\u002F\u002Ftelegram.org\u002Fjs\u002Ftelegram-web-app.js'",[81,906,907],{"class":91},", defer: ",[81,909,819],{"class":135},[81,911,912],{"class":91}," }\n",[81,914,915],{"class":83,"line":141},[81,916,917],{"class":91},"  ]\n",[81,919,920],{"class":83,"line":154},[81,921,922],{"class":91},"})\n",[864,924,926],{"id":925},"step-2-access-the-webapp-bridge-in-vue","Step 2: Access the WebApp Bridge in Vue",[11,928,929],{},"Create a Vue composable to access and synchronize Telegram styles:",[34,931,933],{"className":75,"code":932,"language":77,"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 Request the container to fill maximum vertical space\n      \n      user.value = tg.initDataUnsafe?.user;\n      isReady.value = true;\n\n      \u002F\u002F Apply Telegram theme colors to 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,934,935,940,954,958,970,988,1012,1016,1029,1050,1058,1068,1081,1086,1097,1109,1113,1118,1131,1147,1161,1175,1189,1194,1199,1203,1211],{"__ignoreMap":40},[81,936,937],{"class":83,"line":84},[81,938,939],{"class":395},"\u002F\u002F composables\u002FuseTelegram.ts\n",[81,941,942,944,947,949,952],{"class":83,"line":105},[81,943,88],{"class":87},[81,945,946],{"class":91}," { ref, onMounted } ",[81,948,95],{"class":87},[81,950,951],{"class":98}," 'vue'",[81,953,102],{"class":91},[81,955,956],{"class":83,"line":112},[81,957,109],{"emptyLinePlaceholder":108},[81,959,960,962,964,967],{"class":83,"line":125},[81,961,262],{"class":87},[81,963,265],{"class":87},[81,965,966],{"class":118}," useTelegram",[81,968,969],{"class":91},"() {\n",[81,971,972,974,977,979,982,984,986],{"class":83,"line":141},[81,973,303],{"class":87},[81,975,976],{"class":135}," isReady",[81,978,309],{"class":87},[81,980,981],{"class":118}," ref",[81,983,271],{"class":91},[81,985,375],{"class":135},[81,987,342],{"class":91},[81,989,990,992,994,996,998,1001,1004,1007,1010],{"class":83,"line":154},[81,991,303],{"class":87},[81,993,779],{"class":135},[81,995,309],{"class":87},[81,997,981],{"class":118},[81,999,1000],{"class":91},"\u003C",[81,1002,1003],{"class":135},"any",[81,1005,1006],{"class":91},">(",[81,1008,1009],{"class":135},"null",[81,1011,342],{"class":91},[81,1013,1014],{"class":83,"line":167},[81,1015,109],{"emptyLinePlaceholder":108},[81,1017,1018,1021,1024,1027],{"class":83,"line":179},[81,1019,1020],{"class":118},"  onMounted",[81,1022,1023],{"class":91},"(() ",[81,1025,1026],{"class":87},"=>",[81,1028,122],{"class":91},[81,1030,1031,1033,1036,1038,1041,1044,1047],{"class":83,"line":191},[81,1032,755],{"class":87},[81,1034,1035],{"class":135}," tg",[81,1037,309],{"class":87},[81,1039,1040],{"class":91}," (window ",[81,1042,1043],{"class":87},"as",[81,1045,1046],{"class":135}," any",[81,1048,1049],{"class":91},").Telegram?.WebApp;\n",[81,1051,1052,1055],{"class":83,"line":204},[81,1053,1054],{"class":87},"    if",[81,1056,1057],{"class":91}," (tg) {\n",[81,1059,1060,1063,1066],{"class":83,"line":210},[81,1061,1062],{"class":91},"      tg.",[81,1064,1065],{"class":118},"ready",[81,1067,449],{"class":91},[81,1069,1070,1072,1075,1078],{"class":83,"line":215},[81,1071,1062],{"class":91},[81,1073,1074],{"class":118},"expand",[81,1076,1077],{"class":91},"(); ",[81,1079,1080],{"class":395},"\u002F\u002F Request the container to fill maximum vertical space\n",[81,1082,1083],{"class":83,"line":225},[81,1084,1085],{"class":91},"      \n",[81,1087,1088,1091,1094],{"class":83,"line":237},[81,1089,1090],{"class":91},"      user.value ",[81,1092,1093],{"class":87},"=",[81,1095,1096],{"class":91}," tg.initDataUnsafe?.user;\n",[81,1098,1099,1102,1104,1107],{"class":83,"line":249},[81,1100,1101],{"class":91},"      isReady.value ",[81,1103,1093],{"class":87},[81,1105,1106],{"class":135}," true",[81,1108,102],{"class":91},[81,1110,1111],{"class":83,"line":254},[81,1112,109],{"emptyLinePlaceholder":108},[81,1114,1115],{"class":83,"line":259},[81,1116,1117],{"class":395},"      \u002F\u002F Apply Telegram theme colors to CSS custom properties\n",[81,1119,1120,1123,1126,1128],{"class":83,"line":300},[81,1121,1122],{"class":87},"      const",[81,1124,1125],{"class":135}," root",[81,1127,309],{"class":87},[81,1129,1130],{"class":91}," document.documentElement;\n",[81,1132,1133,1136,1139,1141,1144],{"class":83,"line":321},[81,1134,1135],{"class":91},"      root.style.",[81,1137,1138],{"class":118},"setProperty",[81,1140,271],{"class":91},[81,1142,1143],{"class":98},"'--color-tg-bg'",[81,1145,1146],{"class":91},", tg.themeParams.bg_color);\n",[81,1148,1149,1151,1153,1155,1158],{"class":83,"line":345},[81,1150,1135],{"class":91},[81,1152,1138],{"class":118},[81,1154,271],{"class":91},[81,1156,1157],{"class":98},"'--color-tg-text'",[81,1159,1160],{"class":91},", tg.themeParams.text_color);\n",[81,1162,1163,1165,1167,1169,1172],{"class":83,"line":351},[81,1164,1135],{"class":91},[81,1166,1138],{"class":118},[81,1168,271],{"class":91},[81,1170,1171],{"class":98},"'--color-tg-button'",[81,1173,1174],{"class":91},", tg.themeParams.button_color);\n",[81,1176,1177,1179,1181,1183,1186],{"class":83,"line":366},[81,1178,1135],{"class":91},[81,1180,1138],{"class":118},[81,1182,271],{"class":91},[81,1184,1185],{"class":98},"'--color-tg-button-text'",[81,1187,1188],{"class":91},", tg.themeParams.button_text_color);\n",[81,1190,1191],{"class":83,"line":381},[81,1192,1193],{"class":91},"    }\n",[81,1195,1196],{"class":83,"line":387},[81,1197,1198],{"class":91},"  });\n",[81,1200,1201],{"class":83,"line":392},[81,1202,109],{"emptyLinePlaceholder":108},[81,1204,1205,1208],{"class":83,"line":399},[81,1206,1207],{"class":87},"  return",[81,1209,1210],{"class":91}," { isReady, user };\n",[81,1212,1213],{"class":83,"line":452},[81,1214,207],{"class":91},[26,1216,1218],{"id":1217},"_3-monetization-payments-via-telegram-stars","3. Monetization: Payments via Telegram Stars",[11,1220,1221,1222,357,1225,1228],{},"When operating inside Telegram, all digital services or content purchases must comply with Apple App Store and Google Play policies. Telegram enforces this by requiring the use of ",[15,1223,1224],{},"Telegram Stars",[42,1226,1227],{},"XTR",") for digital goods.",[11,1230,1231],{},"The flow for receiving Stars payments:",[1233,1234,1235,1242,1254,1260],"ol",{},[1236,1237,1238,1241],"li",{},[15,1239,1240],{},"Request Invoice",": The TWA requests the backend to generate an invoice.",[1236,1243,1244,1247,1248,1251,1252,62],{},[15,1245,1246],{},"Bot Sends Invoice",": The backend calls Bot API ",[42,1249,1250],{},"sendInvoice"," using the currency ",[42,1253,1227],{},[1236,1255,1256,1259],{},[15,1257,1258],{},"Client Checkout",": The Telegram client opens a native overlay allowing the user to pay using Stars purchased in-app.",[1236,1261,1262,1265,1266,1269],{},[15,1263,1264],{},"Verification",": Telegram sends a webhook ",[42,1267,1268],{},"successful_payment"," to your bot, which credits the user's account in your DB.",[11,1271,1272,1273,1278,1279,62],{},"In my Telegram bot hosting platform, ",[1274,1275,1277],"a",{"href":1276},"\u002Fprojects\u002Ftelego","TeleGo.io",", we provide exactly these billing options, allowing users to spin up their own TWAs and receive Stars instantly. Additionally, we support hybrid human-AI helpdesks, which you can learn to build in my guide on ",[1274,1280,1282],{"href":1281},"\u002Fblog\u002Ftelegram-bot-ai-rag-support","RAG AI Customer Support Bots",[11,1284,1285,1286,1290],{},"For physical goods and consulting services, traditional gateways can be used. Read my ",[1274,1287,1289],{"href":1288},"\u002Fblog\u002Fsaas-stripe-billing-integration","SaaS Stripe Billing Integration guide"," for Express\u002FTypeScript templates.",[26,1292,1294],{"id":1293},"sources-and-documentation","Sources and documentation",[1296,1297,1298,1307],"ul",{},[1236,1299,1300,1306],{},[1274,1301,1305],{"href":1302,"rel":1303},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fwebapps",[1304],"nofollow","Telegram Mini Apps"," — the official WebApp SDK and initData documentation",[1236,1308,1309,1314],{},[1274,1310,1313],{"href":1311,"rel":1312},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fpayments",[1304],"Bot Payments API"," — invoices, Telegram Stars, and payment webhooks",[11,1316,1317],{},"By combining native web frameworks with the Telegram WebApp API, developers can ship complex SaaS platforms and digital products directly into active messaging channels.",[11,1319,1320,1321,1325,1326,1330],{},"If you are planning to build a high-performance Telegram Mini App with secure billing integrations and dynamic frontend design, check out my ",[1274,1322,1324],{"href":1323},"\u002Ftelegram-bots","Telegram Bot Development Service"," or request a ",[1274,1327,1329],{"href":1328},"\u002Fconsultations","Technical Architecture Consultation"," to get a production blueprint.",[1332,1333,1334],"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":105,"depth":105,"links":1336},[1337,1338,1340,1344,1345],{"id":28,"depth":105,"text":29},{"id":46,"depth":105,"text":1339},"1. Security First: Validating the initData Payload",{"id":858,"depth":105,"text":859,"children":1341},[1342,1343],{"id":866,"depth":112,"text":867},{"id":925,"depth":112,"text":926},{"id":1217,"depth":105,"text":1218},{"id":1293,"depth":105,"text":1294},"2026-07-05","A comprehensive developer's guide to using Telegram Mini Apps as a dynamic web frontend, implementing secure hash validation, and processing in-app purchases.","md",[1350,1353,1356],{"q":1351,"a":1352},"How is a Telegram Mini App different from a regular bot?","A bot communicates through messages and buttons, while a Mini App opens a full web interface inside Telegram: dashboards, catalogs, forms, charts. It's an SPA running in a WebView with its own frontend, and authorization happens automatically through the Telegram account.",{"q":1354,"a":1355},"How do I verify that user data in a Mini App isn't forged?","Through cryptographic initData validation: Telegram signs the user data with an HMAC-SHA256 signature derived from the bot token. The backend must recompute and compare the signature on every request — without it, anyone can impersonate any user.",{"q":1357,"a":1358},"When are Telegram Stars mandatory, and when can I use Stripe?","Digital goods and subscriptions inside a Mini App must be paid with Telegram Stars per Apple and Google policies. Physical goods, services, and consulting can be sold through classic gateways like Stripe.","\u002Fimages\u002Fblog\u002Fblog_telegram_twa.jpg","en",{},"\u002Fblog\u002Fen\u002Ftelegram-web-apps-sells",{"title":6,"description":1347},"blog\u002Fen\u002Ftelegram-web-apps-sells",[1366,1367,1368,1369,1370],"Telegram","Mini Apps","SaaS","Node.js","Frontend","4blcVbDR853MsonsChj72pfs0C4mqm-aL83XkBOZ0io",[1373,1384,1393,1403,1412,1420,1430,1438,1446,1456,1464,1472,1480,1488,1495,1504,1510,1517,1525,1531,1539,1545,1555,1564,1571,1579,1588,1594,1602,1610,1618,1624,1629,1635,1641,1646,1651,1657,1662,1669,1676,1682,1688,1694,1700,1706,1712,1719,1726,1733,1739,1745,1752,1759,1766,1772,1778,1784,1790,1796,1802,1808,1814,1820,1826,1832,1838,1844,1850,1856,1862,1868,1874,1880,1886,1892,1898,1904,1910,1916,1922,1928,1934,1940,1946,1952,1958,1964,1970,1976,1982,1988,1994,2000,2006,2012,2018,2024,2030,2036,2042,2047,2053,2059,2065,2071,2077,2083,2089,2095,2102,2108,2114,2121,2127,2133,2143,2149,2154,2161,2167,2173,2179,2185,2191,2197,2204,2211,2218,2224,2230,2236],{"path":1374,"title":1375,"description":1376,"language":1377,"tags":1378,"date":1383,"lang":1360},"\u002Fsnippets\u002Fen\u002Freact-context-usereducer","React context + useReducer for global state","Replace a state management library with Context and useReducer — a predictable, testable global state pattern without external dependencies.","React.js",[1379,1380,1381,1382],"React","Context","useReducer","State Management","2026-08-07",{"path":1385,"title":1386,"description":1387,"language":1377,"tags":1388,"date":1392,"lang":1360},"\u002Fsnippets\u002Fen\u002Freact-usemediaquery-hook","React useMediaQuery — responsive hook","Track CSS media queries in React components — detect breakpoints, render different UI for mobile\u002Ftablet\u002Fdesktop, and react to orientation changes.",[1379,1389,1390,1391],"Hooks","Responsive","Media Query","2026-08-06",{"path":1394,"title":1395,"description":1396,"language":1397,"tags":1398,"date":1402,"lang":1360},"\u002Fsnippets\u002Fen\u002Ftailwind-animations-keyframes","Tailwind CSS animations — keyframes and transition","Craft custom animations with Tailwind's `animate-*` utilities and `@keyframes` — from micro-interactions to full page transitions — without leaving your config.","Tailwind CSS",[1397,1399,1400,1401],"Animation","Keyframes","UX","2026-08-05",{"path":1404,"title":1405,"description":1406,"language":1397,"tags":1407,"date":1411,"lang":1360},"\u002Fsnippets\u002Fen\u002Ftailwind-custom-theme-variables","Tailwind CSS custom theme with CSS variables","Extend Tailwind's default theme with custom colors, fonts, spacing, and breakpoints — all driven by CSS variables for runtime theme switching.",[1397,1408,1409,1410],"Theme","Customization","Design System","2026-08-04",{"path":1413,"title":1414,"description":1415,"language":1397,"tags":1416,"date":1419,"lang":1360},"\u002Fsnippets\u002Fen\u002Ftailwind-dark-mode-class","Tailwind CSS dark mode with class strategy","Toggle dark mode on demand instead of relying on system preferences — the class strategy lets users switch themes with a button, persisted in localStorage or cookie.",[1397,1417,1408,1418],"Dark Mode","Styling","2026-08-03",{"path":1421,"title":1422,"description":1423,"language":1424,"tags":1425,"date":1429,"lang":1360},"\u002Fsnippets\u002Fen\u002Fprisma-migrations-workflow","Prisma migrations — create, apply, and roll back","Manage database schema changes with Prisma Migrate — create migrations from schema changes, apply them to production, and handle drift.","Prisma",[1424,1426,1427,1428],"ORM","Migrations","Database","2026-08-02",{"path":1431,"title":1432,"description":1433,"language":1424,"tags":1434,"date":1437,"lang":1360},"\u002Fsnippets\u002Fen\u002Fprisma-pagination-cursor-offset","Prisma pagination — cursor and offset","Efficiently paginate large datasets with Prisma — offset pagination for simple lists, cursor-based pagination for infinite scroll and real-time feeds.",[1424,1426,1435,1436],"Pagination","Performance","2026-08-01",{"path":1439,"title":1440,"description":1441,"language":1424,"tags":1442,"date":1445,"lang":1360},"\u002Fsnippets\u002Fen\u002Fprisma-crud-operations","Prisma CRUD — create, read, update, delete","The four basic data operations with Prisma Client — create a user, find by email, update profile, and delete an account — all fully typed.",[1424,1426,1443,1444],"CRUD","TypeScript","2026-07-31",{"path":1447,"title":1448,"description":1449,"language":1450,"tags":1451,"date":1455,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnginx-websocket-proxy","Nginx WebSocket proxy for real-time apps","Proxy WebSocket connections through nginx — required for Socket.io, live chat, and collaborative editing behind a reverse proxy.","Nginx",[1450,1452,1453,1454],"WebSocket","Proxy","Real-time","2026-07-30",{"path":1457,"title":1458,"description":1459,"language":1450,"tags":1460,"date":1463,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnginx-load-balancing-nodejs","Nginx load balancing between Node.js instances","Distribute traffic across multiple Node.js backend instances with round-robin, health checks, and passive failover — no application changes needed.",[1450,1461,1369,1462],"Load Balancing","DevOps","2026-07-29",{"path":1465,"title":1466,"description":1467,"language":1450,"tags":1468,"date":1471,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnginx-static-cache-server","Nginx static file server with caching","Serve static assets (CSS, JS, images) with far-future cache headers, gzip compression, and directory index — no app server needed.",[1450,1469,1470,1436],"Static Files","Caching","2026-07-28",{"path":1473,"title":1474,"description":1475,"language":1476,"tags":1477,"date":1479,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgithub-actions-scheduled-cron","GitHub Actions — scheduled tasks with cron","Run workflows on a schedule — daily backups, weekly dependency updates, or hourly health checks — using cron syntax.","GitHub Actions",[1476,1478,1462],"Automation","2026-07-27",{"path":1481,"title":1482,"description":1483,"language":1476,"tags":1484,"date":1487,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgithub-actions-docker-build-push","GitHub Actions — build and push Docker image to registry","Automatically build and push a Docker image to Docker Hub or GitHub Container Registry on every push to main — with layer caching.",[1476,1485,1486,1462],"Docker","CI\u002FCD","2026-07-26",{"path":1489,"title":1490,"description":1491,"language":1476,"tags":1492,"date":1494,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgithub-actions-ci-pr","GitHub Actions — CI pipeline for PRs","Run linting, type-checking, and tests on every pull request before merge — fail fast with clear annotations on the PR.",[1476,1486,1493,1462],"Testing","2026-07-25",{"path":1496,"title":1497,"description":1498,"language":1499,"tags":1500,"date":1503,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgit-log-filter-format","Git log — filtering and formatting commit history","Search, filter, and format your Git log — find commits by author, date, file, or content with a readable one-line output.","Git",[1499,1501,1502],"Workflow","Debugging","2026-07-24",{"path":1505,"title":1506,"description":1507,"language":1499,"tags":1508,"date":1509,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgit-cherry-pick-bisect","Git cherry-pick and bisect — target specific commits","Cherry-pick applies a specific commit from any branch to your current branch. Bisect performs a binary search through history to find the commit that introduced a bug.",[1499,1501,1502],"2026-07-23",{"path":1511,"title":1512,"description":1513,"language":1499,"tags":1514,"date":1516,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgit-stash-work-in-progress","Git stash — save and restore work in progress","Temporarily save uncommitted changes with git stash — switch branches, pull updates, and restore your work later without losing anything.",[1499,1501,1515],"Version Control","2026-07-22",{"path":1518,"title":1519,"description":1520,"language":1485,"tags":1521,"date":1524,"lang":1360},"\u002Fsnippets\u002Fen\u002Fdocker-security-nonroot","Docker security — non-root user and read-only filesystem","Harden your containers by running as a non-root user with a read-only root filesystem and dropped kernel capabilities.",[1485,1522,1462,1523],"Security","Production","2026-07-21",{"path":1526,"title":1527,"description":1528,"language":1485,"tags":1529,"date":1530,"lang":1360},"\u002Fsnippets\u002Fen\u002Fdocker-multistage-build-nodejs","Docker multi-stage builds for Node.js","A production Dockerfile with multi-stage builds — separate deps, build, and runtime stages for a minimal final image.",[1485,1369,1462,1523],"2026-07-20",{"path":1532,"title":1533,"description":1534,"language":1397,"tags":1535,"date":1538,"lang":1360},"\u002Fsnippets\u002Fen\u002Ftailwind-responsive-card-grid","Tailwind CSS responsive card grid","A responsive card grid with Tailwind CSS that adapts from 1 column on mobile to 4 columns on desktop — no media queries needed.",[1397,1536,1390,1537],"CSS","Grid","2026-07-19",{"path":1540,"title":1541,"description":1542,"language":1499,"tags":1543,"date":1544,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgit-squash-rebase-workflow","Git squash commits and interactive rebase","Clean up your Git history with squash, fixup, and interactive rebase — keep a readable commit log before merging.",[1499,1501,1515],"2026-07-18",{"path":1546,"title":1547,"description":1548,"language":1549,"tags":1550,"date":1554,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnextjs-middleware-auth","Next.js App Router middleware — authentication and redirects","Protect routes with Next.js middleware — check auth tokens, redirect unauthenticated users, and apply i18n routing.","Next.js",[1549,1551,1552,1553],"Auth","Middleware","App Router","2026-07-17",{"path":1556,"title":1557,"description":1558,"language":1369,"tags":1559,"date":1563,"lang":1360},"\u002Fsnippets\u002Fen\u002Fstripe-webhook-idempotency","Stripe webhook handler with idempotency","Process Stripe webhooks safely — verify signatures, deduplicate events, handle checkout.session.completed and subscription updates.",[1369,1560,1561,1562],"Stripe","Webhooks","Payments","2026-07-16",{"path":1565,"title":1566,"description":1567,"language":1476,"tags":1568,"date":1570,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgithub-actions-deploy-vps","GitHub Actions — deploy Node.js app to VPS via SSH","A CI\u002FCD pipeline that lints, tests, builds, and deploys a Node.js app to a VPS over SSH with zero-downtime.",[1476,1486,1462,1569],"Deployment","2026-07-15",{"path":1572,"title":1573,"description":1574,"language":1575,"tags":1576,"date":1578,"lang":1360},"\u002Fsnippets\u002Fen\u002Fexpress-jwt-refresh-tokens","JWT access and refresh token rotation in Express","A complete JWT auth middleware with short-lived access tokens and httpOnly refresh token rotation — no refresh token reuse vulnerability.","Express.js",[1575,1577,1551,1522],"JWT","2026-07-14",{"path":1580,"title":1581,"description":1582,"language":1444,"tags":1583,"date":1587,"lang":1360},"\u002Fsnippets\u002Fen\u002Fzod-api-validation-schema","Zod API validation schemas","Define type-safe request validation with Zod — parse, transform, and validate API request bodies, query strings, and URL params.",[1444,1584,1585,1586],"Zod","Validation","API","2026-07-13",{"path":1589,"title":1590,"description":1591,"language":1424,"tags":1592,"date":1593,"lang":1360},"\u002Fsnippets\u002Fen\u002Fprisma-schema-user-relations","Prisma schema — User, Post, and relations","A practical Prisma schema with User, Post, Session, and Subscription models using relations, enums, and indexes.",[1424,1426,1428,1444],"2026-07-12",{"path":1595,"title":1596,"description":1597,"language":1450,"tags":1598,"date":1601,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnginx-reverse-proxy-ssl","Nginx reverse proxy with SSL termination","A hardened nginx configuration for proxying Node.js apps — SSL termination, rate limiting, security headers, and gzip.",[1450,1462,1599,1600],"SSL","Reverse Proxy","2026-07-11",{"path":1603,"title":1604,"description":1605,"language":1485,"tags":1606,"date":1609,"lang":1360},"\u002Fsnippets\u002Fen\u002Fdocker-compose-node-postgres-redis","Docker Compose for Node.js + PostgreSQL + Redis","A production-style docker-compose.yml for a Node.js app with PostgreSQL and Redis — the standard SaaS backend stack.",[1485,1462,1369,1607,1608],"PostgreSQL","Redis","2026-07-10",{"path":1611,"title":1612,"description":1613,"language":1614,"tags":1615,"date":1617,"lang":1360},"\u002Fsnippets\u002Fen\u002Fvue-use-view-mode","useViewMode hook","Toggle between grid\u002Flist view and persist the choice to localStorage across visits.","Vue",[1614,1616,1389],"JavaScript","2026-07-09",{"path":1619,"title":1620,"description":1621,"language":1614,"tags":1622,"date":1623,"lang":1360},"\u002Fsnippets\u002Fen\u002Fvue-use-platform","usePlatform hook","Detects macOS vs. other platforms so you can show the right keyboard shortcut symbol (⌘ vs Ctrl).",[1614,1616,1389],"2026-07-08",{"path":1625,"title":1626,"description":1627,"language":1614,"tags":1628,"date":1623,"lang":1360},"\u002Fsnippets\u002Fen\u002Fvue-use-theme","useTheme hook","A Vue composable for light\u002Fdark\u002Fsystem theme detection and switching, persisted to localStorage.",[1614,1616,1389],{"path":1630,"title":1631,"description":1632,"language":1614,"tags":1633,"date":1634,"lang":1360},"\u002Fsnippets\u002Fen\u002Fvue-use-media-query","useMediaQuery — Reactive Media Query","A Vue composable that returns a reactive ref updated whenever a CSS media query result changes. SSR-safe — does not throw on the server.",[1614,1616,1389,1536],"2026-07-07",{"path":1636,"title":1637,"description":1638,"language":1614,"tags":1639,"date":1640,"lang":1360},"\u002Fsnippets\u002Fen\u002Fvue-use-drag-and-drop","useDragAndDrop hook","A Vue composable that wires up drag-and-drop file uploads — tracks drag state and hands dropped files to your callback.",[1614,1616,1389],"2026-07-06",{"path":1642,"title":1643,"description":1644,"language":1614,"tags":1645,"date":1640,"lang":1360},"\u002Fsnippets\u002Fen\u002Fvue-use-intersection-observer","useIntersectionObserver — Vue Composable","A Vue composable for tracking element visibility in the viewport — great for lazy-loading images, scroll animations, and infinite scroll.",[1614,1616,1389,1436],{"path":1647,"title":1648,"description":1649,"language":1614,"tags":1650,"date":1346,"lang":1360},"\u002Fsnippets\u002Fen\u002Fvue-use-debounced-ref","useDebouncedRef — Delayed Reactive Value","A Vue composable returning a pair of refs: a \"live\" value and a debounced version that only updates after a pause in input. Perfect for search queries and filters.",[1614,1616,1389,1436],{"path":1652,"title":1653,"description":1654,"language":1614,"tags":1655,"date":1656,"lang":1360},"\u002Fsnippets\u002Fen\u002Fvue-use-breakpoints","useBreakpoints hook","A Vue composable that reactively tracks the viewport width and exposes isMobile\u002FisTablet\u002FisDesktop flags.",[1614,1616,1389],"2026-07-04",{"path":1658,"title":1659,"description":1660,"language":1614,"tags":1661,"date":1656,"lang":1360},"\u002Fsnippets\u002Fen\u002Fvue-use-clipboard-copy","useClipboardCopy hook","A Vue composable that copies text to the clipboard with a fallback for older browsers and a brief \"copied\" state.",[1614,1616,1389],{"path":1663,"title":1664,"description":1665,"language":1444,"tags":1666,"date":1668,"lang":1360},"\u002Fsnippets\u002Fen\u002Ftypescript-use-fetch-with-abort","useFetch with AbortController — TypeScript","A Vue composable for fetch requests that automatically cancels the previous request when a new one is made. Reactive data, loading, and error states included.",[1444,1614,1389,1586,1667],"Async","2026-07-03",{"path":1670,"title":1671,"description":1672,"language":1444,"tags":1673,"date":1675,"lang":1360},"\u002Fsnippets\u002Fen\u002Ftypescript-pagination-offset-limit","Pagination Utilities (Offset \u002F Limit)","Typed helpers for server-side pagination — compute offset and limit from a page number, return metadata (totalPages, hasNext, hasPrev).",[1444,1674,1586],"Utils","2026-07-02",{"path":1677,"title":1678,"description":1679,"language":1444,"tags":1680,"date":1681,"lang":1360},"\u002Fsnippets\u002Fen\u002Ftypescript-group-by-key","Group Array by Key","A lodash-free groupBy — groups an array of objects by a field value into a Record. TypeScript-typed, with a note on the native Object.groupBy alternative.",[1616,1444,1674],"2026-07-01",{"path":1683,"title":1684,"description":1685,"language":1444,"tags":1686,"date":1687,"lang":1360},"\u002Fsnippets\u002Fen\u002Ftypescript-chunk-array","Chunk Array into Groups","Splits an array into groups of a given size — useful for batch processing, in-memory pagination, or sending data in parts.",[1616,1444,1674],"2026-06-30",{"path":1689,"title":1690,"description":1691,"language":1377,"tags":1692,"date":1693,"lang":1360},"\u002Fsnippets\u002Fen\u002Freact.js-use-local-storage-react","usePersistedState hook","A useState drop-in that syncs its value to localStorage so it survives page reloads.",[1377,1616,1389],"2026-06-29",{"path":1695,"title":1696,"description":1697,"language":1377,"tags":1698,"date":1699,"lang":1360},"\u002Fsnippets\u002Fen\u002Freact.js-use-fetch-hook","useFetch hook","A reusable hook for fetching data with loading\u002Ferror state, handling request cancellation when the component unmounts or the URL changes.",[1377,1616,1389],"2026-06-28",{"path":1701,"title":1702,"description":1703,"language":1377,"tags":1704,"date":1705,"lang":1360},"\u002Fsnippets\u002Fen\u002Freact.js-use-debounce-hook","useDebounce hook","A React hook that debounces a fast-changing value (like search input) so effects only run once the user stops typing.",[1377,1616,1389],"2026-06-27",{"path":1707,"title":1708,"description":1709,"language":1377,"tags":1710,"date":1711,"lang":1360},"\u002Fsnippets\u002Fen\u002Freact.js-use-click-outside-hook","useOnClickOutside hook","Detects a click outside a referenced element — the standard way to close dropdowns, modals, and popovers.",[1377,1616,1389],"2026-06-26",{"path":1713,"title":1714,"description":1715,"language":1716,"tags":1717,"date":1718,"lang":1360},"\u002Fsnippets\u002Fen\u002Fpython-retry-decorator-python","Retry Decorator with Backoff (Python)","A decorator for retrying unstable functions with exponential backoff and jitter. Configurable exceptions, attempt count, and delay.","Python",[1716,1674,1586,1667],"2026-06-25",{"path":1720,"title":1721,"description":1722,"language":1716,"tags":1723,"date":1725,"lang":1360},"\u002Fsnippets\u002Fen\u002Fpython-read-csv-file","Read a CSV file into a list of dictionaries","The standard way to parse a CSV file's header row into keys, using Python's built-in csv module — no pandas needed for simple cases.",[1716,1724],"Files","2026-06-24",{"path":1727,"title":1728,"description":1729,"language":1716,"tags":1730,"date":1732,"lang":1360},"\u002Fsnippets\u002Fen\u002Fpython-flatten-nested-list-python","Flatten a nested list","Recursively flatten an arbitrarily nested list into a single flat list.",[1716,1731],"Utility","2026-06-23",{"path":1734,"title":1735,"description":1736,"language":1716,"tags":1737,"date":1738,"lang":1360},"\u002Fsnippets\u002Fen\u002Fpython-check-file-exists","Check if a file or directory exists","The modern, cross-platform way to check for a file's existence using pathlib instead of the older os.path functions.",[1716,1724],"2026-06-22",{"path":1740,"title":1741,"description":1742,"language":1716,"tags":1743,"date":1744,"lang":1360},"\u002Fsnippets\u002Fen\u002Fpython-async-gather-limited-python","asyncio.gather with Concurrency Limit","Runs a list of async tasks in parallel, but no more than N at a time — using asyncio.Semaphore. Prevents overloading external APIs or databases when processing large task lists.",[1716,1667,1674,1586],"2026-06-21",{"path":1746,"title":1747,"description":1748,"language":1749,"tags":1750,"date":1751,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-validate-email","Validate an email address","The correct built-in way to check an email's format in PHP — no need for a hand-rolled regex.","PHP",[1749,1731],"2026-06-20",{"path":1753,"title":1754,"description":1755,"language":1749,"tags":1756,"date":1758,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-timezone-gmt-offset","Get a timezone's current GMT offset in PHP","Turns an IANA timezone name into a friendly label like \"(GMT+02:00) Asia\u002FJerusalem\", accounting for daylight saving time.",[1749,1731,1757],"i18n","2026-06-19",{"path":1760,"title":1761,"description":1762,"language":1749,"tags":1763,"date":1765,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-telegram-webhook-verify-secret","Verify Telegram Webhook Secret","Validates the X-Telegram-Bot-Api-Secret-Token header on an incoming webhook request — protects the endpoint from requests that don't come from Telegram.",[1749,1366,1764,1522],"Bots","2026-06-18",{"path":1767,"title":1768,"description":1769,"language":1749,"tags":1770,"date":1771,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-telegram-send-message-php","Send Telegram Message via Bot API (PHP)","Sends a message through the Telegram Bot API without any SDK — only built-in curl. Supports MarkdownV2, ReplyMarkup, reply_to_message_id, and any other sendMessage parameters.",[1749,1366,1764,1586],"2026-06-17",{"path":1773,"title":1774,"description":1775,"language":1749,"tags":1776,"date":1777,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-telegram-get-user-php","Get Telegram User ID via PHP Webhook","Reads an incoming Telegram update from php:\u002F\u002Finput and returns a normalised user object — id, username, name, and language code. Works with message, callback_query, and other update types.",[1749,1366,1764],"2026-06-16",{"path":1779,"title":1780,"description":1781,"language":1749,"tags":1782,"date":1783,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-secure-random-password-php","Generate a secure random password","Create a cryptographically secure random password using PHP's random_bytes() and bin2hex().",[1749,1522],"2026-06-15",{"path":1785,"title":1786,"description":1787,"language":1749,"tags":1788,"date":1789,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-sanitize-utf8-php","Sanitize a string to valid UTF-8","Strips or repairs invalid byte sequences so a string is always safe to store, log, or return as JSON.",[1749,1731],"2026-06-14",{"path":1791,"title":1792,"description":1793,"language":1749,"tags":1794,"date":1795,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-retry-with-backoff","Retry an operation with exponential backoff + jitter","Retries a flaky API call with increasing delays between attempts, plus random jitter so many clients don't retry in lockstep.",[1749,1586],"2026-06-13",{"path":1797,"title":1798,"description":1799,"language":1749,"tags":1800,"date":1801,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-rate-limiter-redis","Redis Rate Limiter (PHP)","A simple Redis-based rate limiter — restricts the number of requests from a user within a sliding time window. Atomic increment via INCR\u002FEXPIRE with no extra dependencies.",[1749,1608,1586,1522],"2026-06-12",{"path":1803,"title":1804,"description":1805,"language":1749,"tags":1806,"date":1807,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-parse-bcp47-language-tag","Parse a BCP-47 language tag","Normalizes language codes like en-GB, pt_BR, or sr_RS (as sent by browsers, Telegram, etc.) into a language + region pair.",[1749,1731],"2026-06-11",{"path":1809,"title":1810,"description":1811,"language":1749,"tags":1812,"date":1813,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-mask-token-preview","Mask a token\u002FAPI key for safe display","Shows only the first and last 4 characters of a secret (e.g. an API key) so it's safe to log or display in a UI.",[1749,1522],"2026-06-10",{"path":1815,"title":1816,"description":1817,"language":1749,"tags":1818,"date":1819,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-ip-geolocation-php","Get IP geolocation info (ipapi.co)","Look up a visitor's city, country and timezone from their IP address using the free ipapi.co API.",[1749,1586],"2026-06-09",{"path":1821,"title":1822,"description":1823,"language":1749,"tags":1824,"date":1825,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-initials-avatar-php","Generate a colored initials avatar (HTML\u002FCSS)","Renders a circular avatar with the user's initials and a color derived from their name — a lightweight fallback when there's no profile photo.",[1749,1731],"2026-06-08",{"path":1827,"title":1828,"description":1829,"language":1749,"tags":1830,"date":1831,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-hash-password-bcrypt-php","Bcrypt Password Hashing (PHP)","Hashes and verifies passwords using PHP's native password_hash with bcrypt (cost 12). Includes a rehash check for when hashing parameters change.",[1749,1522,1551],"2026-06-07",{"path":1833,"title":1834,"description":1835,"language":1749,"tags":1836,"date":1837,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-get-client-ip-behind-proxy","Get the real client IP behind a proxy\u002FCDN","Reads the visitor's real IP from CDN\u002Fproxy headers (Cloudflare, X-Forwarded-For, etc.), falling back to $_SERVER['REMOTE_ADDR'], and rejects private\u002Fspoofed values.",[1749,1522],"2026-06-06",{"path":1839,"title":1840,"description":1841,"language":1749,"tags":1842,"date":1843,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-format-bytes-human-readable","Format bytes as a human-readable size","Turns a raw byte count into \"1.5 MB\", \"820 KB\", etc. — the standard file-size display helper.",[1749,1731],"2026-06-05",{"path":1845,"title":1846,"description":1847,"language":1749,"tags":1848,"date":1849,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-escape-telegram-markdownv2","Escape text for Telegram MarkdownV2","Telegram's Bot API rejects messages containing unescaped MarkdownV2 special characters — this escapes all of them safely.",[1749,1586,1366],"2026-06-04",{"path":1851,"title":1852,"description":1853,"language":1749,"tags":1854,"date":1855,"lang":1360},"\u002Fsnippets\u002Fen\u002Fphp-curl-get-request","Make a GET request with cURL","The classic PHP recipe for fetching JSON from an external API without any extra dependencies.",[1749,1586],"2026-06-03",{"path":1857,"title":1858,"description":1859,"language":1369,"tags":1860,"date":1861,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnode.js-timing-safe-string-compare","Constant-time string comparison for secrets","Compares two secrets (API tokens, webhook signatures) without leaking timing information that could help an attacker guess them character by character.",[1369,1522],"2026-06-02",{"path":1863,"title":1864,"description":1865,"language":1369,"tags":1866,"date":1867,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnode.js-read-file-async","Read a file asynchronously","The modern Promise-based way to read a file's contents in Node.js — no callbacks, and no blocking the event loop like fs.readFileSync would.",[1369,1724],"2026-06-01",{"path":1869,"title":1870,"description":1871,"language":1369,"tags":1872,"date":1873,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnode.js-read-env-defaults-node","Read environment variables with defaults","A tiny helper that reads a Node.js environment variable and falls back to a default when it's missing.",[1369,1616],"2026-05-31",{"path":1875,"title":1876,"description":1877,"language":1369,"tags":1878,"date":1879,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnode.js-hash-password-bcrypt","Hash and verify a password with bcrypt","The standard way to store passwords safely in Node.js — never store or compare plain text.",[1369,1522],"2026-05-30",{"path":1881,"title":1882,"description":1883,"language":1369,"tags":1884,"date":1885,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnode.js-create-http-server","Create a basic HTTP server","The bare Node.js way to serve requests — no Express needed for something this simple.",[1369,1586],"2026-05-29",{"path":1887,"title":1888,"description":1889,"language":1549,"tags":1890,"date":1891,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnext.js-redirect","Redirect from a Server Component or Route Handler","The App Router's built-in way to send a user to another page — no need to construct a Response manually.",[1549,1377],"2026-05-28",{"path":1893,"title":1894,"description":1895,"language":1549,"tags":1896,"date":1897,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnext.js-read-server-search-params-nextjs","Read search params in a Server Component","Access the current URL's query string inside an App Router Server Component without client-side JavaScript.",[1549,1377],"2026-05-27",{"path":1899,"title":1900,"description":1901,"language":1549,"tags":1902,"date":1903,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnext.js-dynamic-metadata","Generate dynamic page metadata","Sets a page's \u003Ctitle> and meta tags based on fetched data — the App Router replacement for next\u002Fhead.",[1549,1377],"2026-05-26",{"path":1905,"title":1906,"description":1907,"language":1549,"tags":1908,"date":1909,"lang":1360},"\u002Fsnippets\u002Fen\u002Fnext.js-api-route-handler","Create an API route handler","The App Router way to build a REST endpoint — export a function named after the HTTP method instead of a single catch-all handler.",[1549,1586],"2026-05-25",{"path":1911,"title":1912,"description":1913,"language":1616,"tags":1914,"date":1915,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-validate-media-url","Validate a media URL","Checks that a URL is HTTPS and points to a recognized image\u002Fvideo\u002Faudio file extension — a common guard before accepting a user-supplied media link.",[1616,1731],"2026-05-24",{"path":1917,"title":1918,"description":1919,"language":1616,"tags":1920,"date":1921,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-validate-email","Validate Email Address","Checks email correctness without an over-complicated regex — strict enough for forms and APIs without rejecting rare but valid addresses.",[1616,1444,1674,1585],"2026-05-23",{"path":1923,"title":1924,"description":1925,"language":1616,"tags":1926,"date":1927,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-truncate-text-by-words","Truncate Text by Word Count","Truncates a string to a given number of words and appends a suffix (\"…\" by default). Unlike character truncation, it never splits a word in the middle.",[1616,1444,1674],"2026-05-22",{"path":1929,"title":1930,"description":1931,"language":1616,"tags":1932,"date":1933,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-to-boolean","Parse loose values into a real boolean","Turns strings like \"yes\"\u002F\"on\"\u002F\"1\" (and numbers) into a proper boolean — handy for parsing env vars, query params, or CSV imports.",[1616,1731],"2026-05-21",{"path":1935,"title":1936,"description":1937,"language":1616,"tags":1938,"date":1939,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-timezone-gmt-offset-label","Display a timezone with its GMT offset","Turns an IANA timezone name into a friendly label like \"(GMT+02:00) Asia\u002FJerusalem\" for timezone pickers.",[1616,1731,1757],"2026-05-20",{"path":1941,"title":1942,"description":1943,"language":1616,"tags":1944,"date":1945,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-throttle-function","Throttle a function","Ensures a function runs at most once every X milliseconds — ideal for scroll and mousemove handlers that would otherwise fire hundreds of times a second.",[1616,1731],"2026-05-19",{"path":1947,"title":1948,"description":1949,"language":1616,"tags":1950,"date":1951,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-telegram-user-link","Telegram User Link and Mention","Generates a clickable link to a Telegram user profile and mention markup for MarkdownV2 — via username if available, otherwise via a tg:\u002F\u002Fuser?id= deep link.",[1616,1366,1764],"2026-05-18",{"path":1953,"title":1954,"description":1955,"language":1616,"tags":1956,"date":1957,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-telegram-get-user-from-update","Get User ID and Data from Telegram Update","Extracts the user ID, name, and username from any type of incoming Telegram update — message, callback_query, edited_message, inline_query, and others.",[1616,1366,1764],"2026-05-17",{"path":1959,"title":1960,"description":1961,"language":1616,"tags":1962,"date":1963,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-telegram-answer-callback-query","Answer Telegram Callback Query","Responds to an inline button press (answerCallbackQuery) without an SDK — removes the loading spinner from the button, optionally showing a notification or alert to the user.",[1616,1366,1764,1586],"2026-05-16",{"path":1965,"title":1966,"description":1967,"language":1616,"tags":1968,"date":1969,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-smart-telegram-button-layout","Auto-arrange Telegram inline keyboard buttons","Packs short buttons two-per-row and gives long ones their own row, based on the message text length above them — keeps keyboards compact without truncating labels.",[1616,1366],"2026-05-15",{"path":1971,"title":1972,"description":1973,"language":1616,"tags":1974,"date":1975,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-sleep-delay-async","sleep() \u002F Async Delay","A minimal sleep function for JavaScript and TypeScript — pauses an async function for a given number of milliseconds without any third-party library.",[1616,1444,1674,1667],"2026-05-14",{"path":1977,"title":1978,"description":1979,"language":1616,"tags":1980,"date":1981,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-seconds-until-utc-rollover","Get seconds until the next UTC day\u002Fmonth rollover","Computes how many seconds remain until midnight UTC (or the 1st of next month) — the exact TTL you want on a Redis key for a daily\u002Fmonthly quota.",[1616,1731],"2026-05-13",{"path":1983,"title":1984,"description":1985,"language":1616,"tags":1986,"date":1987,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-scroll-to-anchor","Smooth Scroll to Anchor","Scrolls the page to an element by ID, accounting for an offset (e.g. a fixed header height) — no jQuery, native smooth scroll.",[1616,1536,1674],"2026-05-12",{"path":1989,"title":1990,"description":1991,"language":1616,"tags":1992,"date":1993,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-safe-json-parse","A JSON.parse that never throws","Wraps JSON.parse to return null on invalid input instead of crashing — perfect for deserializing values from Redis, localStorage, or user input.",[1616,1731],"2026-05-11",{"path":1995,"title":1996,"description":1997,"language":1616,"tags":1998,"date":1999,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-remove-duplicate-array-items","Remove duplicates from an array","The one-line way to deduplicate an array of primitives using Set, plus the pattern for deduplicating an array of objects by a key.",[1616,1731],"2026-05-10",{"path":2001,"title":2002,"description":2003,"language":1616,"tags":2004,"date":2005,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-pluralize-intl","Pluralize words with Intl.PluralRules","Correctly pluralizes nouns for any locale — including languages like Russian with multiple plural forms — using the built-in Intl API.",[1616,1731,1757],"2026-05-09",{"path":2007,"title":2008,"description":2009,"language":1616,"tags":2010,"date":2011,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-pick-omit-object-keys","Pick and omit object keys","Two small helpers for building a new object with only (or without) a specific set of keys — handy for trimming payloads before sending them to an API.",[1616,1731],"2026-05-08",{"path":2013,"title":2014,"description":2015,"language":1616,"tags":2016,"date":2017,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-parse-telegram-start-params","Parse Telegram \u002Fstart deep-link parameters","Extracts traffic source, UTM tags, or a referrer ID from the payload Telegram passes after \u002Fstart — the standard way to track where bot users came from.",[1616,1366],"2026-05-07",{"path":2019,"title":2020,"description":2021,"language":1616,"tags":2022,"date":2023,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-jwt-decode-payload","Decode JWT Payload Without Verification","Extracts the payload from a JWT token on the client without verifying the signature — handy for reading exp, userId, and roles in UI. Never use for server-side authorization.",[1616,1444,1551,1577],"2026-05-06",{"path":2025,"title":2026,"description":2027,"language":1616,"tags":2028,"date":2029,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-iso-date-without-timezone-shift","Convert Date to\u002Ffrom \"YYYY-MM-DD\" without timezone shifts","date.toISOString().slice(0,10) silently shifts to UTC and can show yesterday's date near midnight — these two helpers stay in local time.",[1616,1731],"2026-05-05",{"path":2031,"title":2032,"description":2033,"language":1616,"tags":2034,"date":2035,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-is-editable-target","Check if an event target is an editable field","Guards global keyboard shortcuts from firing while the user is typing in an input, textarea, select, or contenteditable element.",[1616,1731],"2026-05-04",{"path":2037,"title":2038,"description":2039,"language":1616,"tags":2040,"date":2041,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-is-color-light","Check if a hex color is light or dark","Computes perceived brightness from a hex color to decide whether black or white text reads better on top of it.",[1616,1731],"2026-05-03",{"path":2043,"title":1816,"description":2044,"language":1616,"tags":2045,"date":2046,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-ip-geolocation-js","Look up a visitor's country and city from their IP address using the free ipapi.co API.",[1616,1586],"2026-05-02",{"path":2048,"title":2049,"description":2050,"language":1616,"tags":2051,"date":2052,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-generate-uuid","Generate a UUID","Creates a spec-compliant random UUID (v4) using the built-in Web Crypto API — no external library needed.",[1616,1731],"2026-05-01",{"path":2054,"title":2055,"description":2056,"language":1616,"tags":2057,"date":2058,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-generate-slug-from-string","Generate Slug from String","Converts an arbitrary string (including Cyrillic) into a URL-friendly slug — lowercase letters, no spaces or special characters, hyphens instead of spaces.",[1616,1444,1674],"2026-04-30",{"path":2060,"title":2061,"description":2062,"language":1616,"tags":2063,"date":2064,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-format-date-in-timezone","Format a date\u002Ftime in any IANA timezone","Renders a date as if you were standing in a specific timezone, regardless of the visitor's own local time.",[1616,1731,1757],"2026-04-29",{"path":2066,"title":2067,"description":2068,"language":1616,"tags":2069,"date":2070,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-format-compact-number","Format a number compactly (1.2K, 3M)","Turns large numbers into short, human-readable labels — 600000 becomes 600K.",[1616,1731],"2026-04-28",{"path":2072,"title":2073,"description":2074,"language":1616,"tags":2075,"date":2076,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-detect-visitor-timezone","Detect the visitor's timezone automatically","One line to get the visitor's IANA timezone (e.g. \"Asia\u002FJerusalem\") straight from the browser, no geolocation permission needed.",[1616,1731,1757],"2026-04-27",{"path":2078,"title":2079,"description":2080,"language":1616,"tags":2081,"date":2082,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-deep-clone-object","Deep Clone an Object","Creates a fully independent copy of an object with no shared references — uses the native structuredClone where available, falling back to a JSON round-trip for older environments.",[1616,1444,1674],"2026-04-26",{"path":2084,"title":2085,"description":2086,"language":1616,"tags":2087,"date":2088,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-dedupe-sort-tag-list","Manage a deduplicated, sorted tag list","Add\u002Fremove operations for a tag array that always stays unique and alphabetically sorted — handy for tag pickers and filter chips.",[1616,1731],"2026-04-25",{"path":2090,"title":2091,"description":2092,"language":1616,"tags":2093,"date":2094,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-debounce-function","Debounce a function","Delay a function's execution until a burst of calls has stopped — ideal for search inputs and resize handlers.",[1616,1731],"2026-04-24",{"path":2096,"title":2097,"description":2098,"language":1616,"tags":2099,"date":2101,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-cosine-similarity","Cosine similarity between two vectors","Measures how similar two embedding vectors are — the core operation behind semantic search and RAG retrieval.",[1616,2100,1731],"AI","2026-04-23",{"path":2103,"title":2104,"description":2105,"language":1616,"tags":2106,"date":2107,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-cn-classname-merge","Merge Tailwind classes safely (cn helper)","Combines conditional classes with clsx and resolves conflicting Tailwind utilities with tailwind-merge — the standard \"cn()\" helper used across shadcn\u002Fui-style codebases.",[1616,1536,1731],"2026-04-22",{"path":2109,"title":2110,"description":2111,"language":1616,"tags":2112,"date":2113,"lang":1360},"\u002Fsnippets\u002Fen\u002Fjavascript-capitalize-first-letter","Capitalize the first letter of a string","The short, correct way to uppercase just the first character — plus a version that capitalizes every word.",[1616,1731],"2026-04-21",{"path":2115,"title":2116,"description":2117,"language":2118,"tags":2119,"date":2120,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgo-simple-http-server","Create a simple HTTP server","The standard-library way to serve HTTP routes in Go — no framework needed for a small API.","Go",[2118,1586],"2026-04-20",{"path":2122,"title":2123,"description":2124,"language":2118,"tags":2125,"date":2126,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgo-read-file-lines-go","Read a file line by line","Stream a text file line by line without loading the whole file into memory.",[2118,1724],"2026-04-19",{"path":2128,"title":2129,"description":2130,"language":2118,"tags":2131,"date":2132,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgo-http-get-request","Make an HTTP GET request","The standard-library way to fetch and parse JSON from an API — no third-party HTTP client needed.",[2118,1586],"2026-04-18",{"path":2134,"title":2135,"description":2136,"language":2118,"tags":2137,"date":2142,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgo-graceful-shutdown","HTTP Server with Graceful Shutdown (Go)","Production-ready Go HTTP server template that intercepts SIGINT\u002FSIGTERM signals to shut down cleanly without interrupting active client requests.",[2118,2138,2139,2140,2141],"Golang","HTTP","Backend","Concurrency","2026-04-17",{"path":2144,"title":2145,"description":2146,"language":2118,"tags":2147,"date":2148,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgo-goroutines-waitgroup","Run tasks concurrently with goroutines","The standard pattern for launching multiple goroutines and waiting for all of them to finish, using sync.WaitGroup.",[2118,1731],"2026-04-16",{"path":2150,"title":2049,"description":2151,"language":2118,"tags":2152,"date":2153,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgo-generate-uuid","Creates a random v4 UUID using only the standard library's crypto\u002Frand — no external uuid package required.",[2118,1731],"2026-04-15",{"path":2155,"title":2156,"description":2157,"language":2118,"tags":2158,"date":2160,"lang":1360},"\u002Fsnippets\u002Fen\u002Fgo-context-timeout","HTTP Request with Timeout via Context (Go)","Sending HTTP requests in Go using context.WithTimeout to enforce network deadline limits and prevent dangling goroutines.",[2118,2138,2139,2159,1380],"Timeout","2026-04-14",{"path":2162,"title":2163,"description":2164,"language":1575,"tags":2165,"date":2166,"lang":1360},"\u002Fsnippets\u002Fen\u002Fexpress.js-sanitize-filename-express","Sanitize an uploaded filename","Strip a file extension and normalize the remaining name to a safe, URL-friendly slug before saving it to disk.",[1575,1369,1616],"2026-04-13",{"path":2168,"title":2169,"description":2170,"language":1575,"tags":2171,"date":2172,"lang":1360},"\u002Fsnippets\u002Fen\u002Fexpress.js-rate-limiting-middleware","Rate limit requests","Caps how many requests a single IP can make in a time window — the standard first line of defense against abuse and brute-force attempts.",[1575,1369,1522],"2026-04-12",{"path":2174,"title":2175,"description":2176,"language":1575,"tags":2177,"date":2178,"lang":1360},"\u002Fsnippets\u002Fen\u002Fexpress.js-jwt-auth-middleware","JWT Auth Middleware (Express)","Express middleware for verifying Authorization header JWT token, parsing payload data, and extending the Request object in TypeScript safely.",[1575,1369,1522,1577,1444],"2026-04-11",{"path":2180,"title":2181,"description":2182,"language":1575,"tags":2183,"date":2184,"lang":1360},"\u002Fsnippets\u002Fen\u002Fexpress.js-error-handling-middleware","Centralized error-handling middleware","Catches errors from any route\u002Fmiddleware in one place instead of repeating try\u002Fcatch and error-formatting logic everywhere.",[1575,1369],"2026-04-10",{"path":2186,"title":2187,"description":2188,"language":1575,"tags":2189,"date":2190,"lang":1360},"\u002Fsnippets\u002Fen\u002Fexpress.js-cors-middleware","Enable CORS in Express","The standard way to allow cross-origin requests from your frontend — either quick-and-open, or locked down to specific origins.",[1575,1369,1586],"2026-04-09",{"path":2192,"title":2193,"description":2194,"language":1536,"tags":2195,"date":2196,"lang":1360},"\u002Fsnippets\u002Fen\u002Fcss-truncate-text-ellipsis","Truncate single-line text with an ellipsis","Cuts off overflowing text on one line and adds \"…\" at the end — the classic CSS-only text truncation.",[1536,1731],"2026-04-08",{"path":2198,"title":2199,"description":2200,"language":1536,"tags":2201,"date":2203,"lang":1360},"\u002Fsnippets\u002Fen\u002Fcss-skeleton-loading","Skeleton Loading Animation (CSS)","A shimmer effect for loading skeletons — a pulsing gradient simulates content loading. No JavaScript, pure CSS animation using custom properties.",[1536,2202,1399],"UI","2026-04-07",{"path":2205,"title":2206,"description":2207,"language":1536,"tags":2208,"date":2210,"lang":1360},"\u002Fsnippets\u002Fen\u002Fcss-line-clamp","Multiline text-overflow with Ellipsis","Truncates text after N lines with an ellipsis via -webkit-line-clamp — works in all modern browsers without JavaScript.",[1536,1674,2209],"Typography","2026-04-06",{"path":2212,"title":2213,"description":2214,"language":1536,"tags":2215,"date":2217,"lang":1360},"\u002Fsnippets\u002Fen\u002Fcss-grid-auto-fit","CSS Grid auto-fit — Responsive Grid Without Media Queries","Creates a self-adapting column grid with auto-fit and minmax — cards reflow automatically as the container width changes, with zero breakpoints.",[1536,2216,1390,1537],"Layout","2026-04-05",{"path":2219,"title":2220,"description":2221,"language":1536,"tags":2222,"date":2223,"lang":1360},"\u002Fsnippets\u002Fen\u002Fcss-gradient-text","CSS Gradient Text","Applies a linear or radial gradient to text via background-clip: text — no SVG, no JavaScript. Supported by all modern browsers.",[1536,2202,2209],"2026-04-04",{"path":2225,"title":2226,"description":2227,"language":1536,"tags":2228,"date":2229,"lang":1360},"\u002Fsnippets\u002Fen\u002Fcss-fluid-typography","Fluid Typography with clamp()","Scales font size smoothly between a minimum and maximum value based on viewport width — no media queries, just clamp().",[1536,2209,1390],"2026-04-03",{"path":2231,"title":2232,"description":2233,"language":1536,"tags":2234,"date":2235,"lang":1360},"\u002Fsnippets\u002Fen\u002Fcss-custom-scrollbar","Custom CSS Scrollbar","Styles the scrollbar to match a design system without JavaScript — thin track, rounded thumb, hover state. Includes a variant to hide the scrollbar while keeping scroll behaviour.",[1536,1674,2202],"2026-04-02",{"path":2237,"title":2238,"description":2239,"language":1536,"tags":2240,"date":2241,"lang":1360},"\u002Fsnippets\u002Fen\u002Fcss-center-with-flexbox-css","Center anything with flexbox","A reusable utility class that perfectly centers a single child both horizontally and vertically.",[1536,2216],"2026-04-01",{"id":2243,"title":1834,"body":2244,"date":1837,"description":1835,"extension":1348,"lang":1360,"language":1749,"meta":2601,"navigation":108,"path":1833,"published":108,"seo":2602,"stem":2603,"tags":2604,"__hash__":2605},"snippets\u002Fsnippets\u002Fen\u002Fphp-get-client-ip-behind-proxy.md",{"type":8,"value":2245,"toc":2599},[2246,2258,2596],[11,2247,2248,2249,2253,2254,2257],{},"How it works: proxy\u002FCDN headers are checked in order of trust (Cloudflare's own header first, since it can't be spoofed once Cloudflare sits in front of the app), and every candidate is validated as a real ",[2250,2251,2252],"em",{},"public"," IP before being trusted — private ranges (10.x, 192.168.x, etc.) are rejected so a client can't fake ",[42,2255,2256],{},"X-Forwarded-For: 10.0.0.1"," to hide behind an internal-looking address.",[34,2259,2263],{"className":2260,"code":2261,"language":2262,"meta":40,"style":40},"language-php shiki shiki-themes github-dark","\u003C?php\n\nfunction getClientIp(): ?string\n{\n    $headers = ['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'];\n\n    foreach ($headers as $header) {\n        $value = $_SERVER[$header] ?? null;\n        if (!$value) {\n            continue;\n        }\n\n        \u002F\u002F X-Forwarded-For can contain a comma-separated chain; the first entry is the original client\n        foreach (explode(',', $value) as $candidate) {\n            $ip = trim($candidate);\n            if (isPublicIp($ip)) {\n                return $ip;\n            }\n        }\n    }\n\n    return $_SERVER['REMOTE_ADDR'] ?? null;\n}\n\nfunction isPublicIp(string $ip): bool\n{\n    return (bool) filter_var(\n        $ip,\n        FILTER_VALIDATE_IP,\n        FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE\n    );\n}\n\n\u002F\u002F Example usage\n$ip = getClientIp(); \u002F\u002F \"203.0.113.42\" — never a spoofed private\u002Floopback address\n","php",[42,2264,2265,2273,2277,2293,2298,2329,2333,2346,2364,2376,2383,2388,2392,2397,2420,2433,2446,2454,2459,2463,2467,2471,2490,2494,2498,2518,2522,2540,2545,2553,2564,2569,2573,2577,2582],{"__ignoreMap":40},[81,2266,2267,2270],{"class":83,"line":84},[81,2268,2269],{"class":87},"\u003C?",[81,2271,2272],{"class":135},"php\n",[81,2274,2275],{"class":83,"line":105},[81,2276,109],{"emptyLinePlaceholder":108},[81,2278,2279,2282,2285,2288,2290],{"class":83,"line":112},[81,2280,2281],{"class":87},"function",[81,2283,2284],{"class":118}," getClientIp",[81,2286,2287],{"class":91},"()",[81,2289,132],{"class":87},[81,2291,2292],{"class":87}," ?string\n",[81,2294,2295],{"class":83,"line":125},[81,2296,2297],{"class":91},"{\n",[81,2299,2300,2303,2305,2308,2311,2313,2316,2318,2321,2323,2326],{"class":83,"line":141},[81,2301,2302],{"class":91},"    $headers ",[81,2304,1093],{"class":87},[81,2306,2307],{"class":91}," [",[81,2309,2310],{"class":98},"'HTTP_CF_CONNECTING_IP'",[81,2312,281],{"class":91},[81,2314,2315],{"class":98},"'HTTP_X_FORWARDED_FOR'",[81,2317,281],{"class":91},[81,2319,2320],{"class":98},"'HTTP_X_REAL_IP'",[81,2322,281],{"class":91},[81,2324,2325],{"class":98},"'HTTP_CLIENT_IP'",[81,2327,2328],{"class":91},"];\n",[81,2330,2331],{"class":83,"line":154},[81,2332,109],{"emptyLinePlaceholder":108},[81,2334,2335,2338,2341,2343],{"class":83,"line":167},[81,2336,2337],{"class":87},"    foreach",[81,2339,2340],{"class":91}," ($headers ",[81,2342,1043],{"class":87},[81,2344,2345],{"class":91}," $header) {\n",[81,2347,2348,2351,2353,2356,2359,2362],{"class":83,"line":179},[81,2349,2350],{"class":91},"        $value ",[81,2352,1093],{"class":87},[81,2354,2355],{"class":91}," $_SERVER[$header] ",[81,2357,2358],{"class":87},"??",[81,2360,2361],{"class":135}," null",[81,2363,102],{"class":91},[81,2365,2366,2369,2371,2373],{"class":83,"line":191},[81,2367,2368],{"class":87},"        if",[81,2370,357],{"class":91},[81,2372,360],{"class":87},[81,2374,2375],{"class":91},"$value) {\n",[81,2377,2378,2381],{"class":83,"line":204},[81,2379,2380],{"class":87},"            continue",[81,2382,102],{"class":91},[81,2384,2385],{"class":83,"line":210},[81,2386,2387],{"class":91},"        }\n",[81,2389,2390],{"class":83,"line":215},[81,2391,109],{"emptyLinePlaceholder":108},[81,2393,2394],{"class":83,"line":225},[81,2395,2396],{"class":395},"        \u002F\u002F X-Forwarded-For can contain a comma-separated chain; the first entry is the original client\n",[81,2398,2399,2402,2404,2407,2409,2412,2415,2417],{"class":83,"line":237},[81,2400,2401],{"class":87},"        foreach",[81,2403,357],{"class":91},[81,2405,2406],{"class":135},"explode",[81,2408,271],{"class":91},[81,2410,2411],{"class":98},"','",[81,2413,2414],{"class":91},", $value) ",[81,2416,1043],{"class":87},[81,2418,2419],{"class":91}," $candidate) {\n",[81,2421,2422,2425,2427,2430],{"class":83,"line":249},[81,2423,2424],{"class":91},"            $ip ",[81,2426,1093],{"class":87},[81,2428,2429],{"class":135}," trim",[81,2431,2432],{"class":91},"($candidate);\n",[81,2434,2435,2438,2440,2443],{"class":83,"line":254},[81,2436,2437],{"class":87},"            if",[81,2439,357],{"class":91},[81,2441,2442],{"class":118},"isPublicIp",[81,2444,2445],{"class":91},"($ip)) {\n",[81,2447,2448,2451],{"class":83,"line":259},[81,2449,2450],{"class":87},"                return",[81,2452,2453],{"class":91}," $ip;\n",[81,2455,2456],{"class":83,"line":300},[81,2457,2458],{"class":91},"            }\n",[81,2460,2461],{"class":83,"line":321},[81,2462,2387],{"class":91},[81,2464,2465],{"class":83,"line":345},[81,2466,1193],{"class":91},[81,2468,2469],{"class":83,"line":351},[81,2470,109],{"emptyLinePlaceholder":108},[81,2472,2473,2475,2478,2481,2484,2486,2488],{"class":83,"line":366},[81,2474,369],{"class":87},[81,2476,2477],{"class":91}," $_SERVER[",[81,2479,2480],{"class":98},"'REMOTE_ADDR'",[81,2482,2483],{"class":91},"] ",[81,2485,2358],{"class":87},[81,2487,2361],{"class":135},[81,2489,102],{"class":91},[81,2491,2492],{"class":83,"line":381},[81,2493,207],{"class":91},[81,2495,2496],{"class":83,"line":387},[81,2497,109],{"emptyLinePlaceholder":108},[81,2499,2500,2502,2505,2507,2510,2513,2515],{"class":83,"line":392},[81,2501,2281],{"class":87},[81,2503,2504],{"class":118}," isPublicIp",[81,2506,271],{"class":91},[81,2508,2509],{"class":87},"string",[81,2511,2512],{"class":91}," $ip)",[81,2514,132],{"class":87},[81,2516,2517],{"class":87}," bool\n",[81,2519,2520],{"class":83,"line":399},[81,2521,2297],{"class":91},[81,2523,2524,2526,2528,2531,2534,2537],{"class":83,"line":452},[81,2525,369],{"class":87},[81,2527,357],{"class":91},[81,2529,2530],{"class":87},"bool",[81,2532,2533],{"class":91},") ",[81,2535,2536],{"class":135},"filter_var",[81,2538,2539],{"class":91},"(\n",[81,2541,2542],{"class":83,"line":457},[81,2543,2544],{"class":91},"        $ip,\n",[81,2546,2547,2550],{"class":83,"line":463},[81,2548,2549],{"class":135},"        FILTER_VALIDATE_IP",[81,2551,2552],{"class":91},",\n",[81,2554,2555,2558,2561],{"class":83,"line":476},[81,2556,2557],{"class":135},"        FILTER_FLAG_NO_PRIV_RANGE",[81,2559,2560],{"class":87}," |",[81,2562,2563],{"class":135}," FILTER_FLAG_NO_RES_RANGE\n",[81,2565,2566],{"class":83,"line":518},[81,2567,2568],{"class":91},"    );\n",[81,2570,2571],{"class":83,"line":538},[81,2572,207],{"class":91},[81,2574,2575],{"class":83,"line":543},[81,2576,109],{"emptyLinePlaceholder":108},[81,2578,2579],{"class":83,"line":549},[81,2580,2581],{"class":395},"\u002F\u002F Example usage\n",[81,2583,2584,2587,2589,2591,2593],{"class":83,"line":555},[81,2585,2586],{"class":91},"$ip ",[81,2588,1093],{"class":87},[81,2590,2284],{"class":118},[81,2592,1077],{"class":91},[81,2594,2595],{"class":395},"\u002F\u002F \"203.0.113.42\" — never a spoofed private\u002Floopback address\n",[1332,2597,2598],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}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 .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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":105,"depth":105,"links":2600},[],{},{"title":1834,"description":1835},"snippets\u002Fen\u002Fphp-get-client-ip-behind-proxy",[1749,1522],"OpqGShGsP3S9_X7aNzwTKv0B0TAej0h29RxWkP1ndWk",1784561423361]