[{"data":1,"prerenderedAt":95504},["ShallowReactive",2],{"\u002Fui-nav":3,"\u002Fui-number-of-pages-with-tags":140,"\u002Fui-articles-with-tags":95052},[4,2664,3549,5307,7527,8457,9469,10153,12314,12812,16638,19584,20868,21092,22219,22703,26544,27882,28610,29006,30741,33560,34972,36744,39233,44958,45977,47680,52203,52759,53529,54259,54603,55554,56699,57041,59135,61086,66559,71445,71925,75901,80410,82425,84466,88079,89847,93330,94341],{"id":5,"title":6,"articleTags":7,"author":11,"blog":12,"body":13,"description":2648,"extension":2649,"image":2650,"keywords":2651,"meta":2657,"navigation":515,"path":2658,"published":2659,"readTime":798,"seo":2660,"stem":2661,"type":2662,"__hash__":2663},"content\u002F2021\u002F05\u002F12\u002Fthe-mysterious-this-keyword-in-vueland.md","The mysterious this keyword in Vueland",[8,9,10],"Vue.js","JavaScript","FrontEnd","Aleksandar Trpkovski","post",{"type":14,"value":15,"toc":2620},"minimark",[16,26,51,55,60,67,83,89,103,108,122,127,192,196,229,235,239,310,314,397,406,412,423,429,433,442,470,474,542,546,556,596,600,676,690,694,700,814,820,824,940,953,957,1002,1036,1195,1212,1355,1363,1372,1445,1449,1458,1612,1623,1782,1785,1791,1798,1804,1807,1851,1897,1903,1915,1927,1930,1933,2089,2102,2120,2127,2167,2173,2179,2193,2261,2264,2421,2424,2561,2564,2568,2616],[17,18,20,21,25],"h1",{"id":19},"the-mysterious-this-keyword-in-vueland","The mysterious ",[22,23,24],"code",{},"this"," keyword in Vueland",[27,28,29],"p",{},[30,31,32,36,37,40,41],"em",{},[33,34],"binding",{"value":35},"$document.published"," • ",[33,38],{"value":39},"$document.readTime"," min read — by ",[42,43,44],"strong",{},[45,46,48],"a",{"href":47},"\u002F",[33,49],{"value":50},"$document.author",[52,53],"tag-pills",{":tags":54},"articleTags",[56,57],"audio-player",{":audio-src":58,":transcript-src":59},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F05\u002F12\u002Fthe-mysterious-this-keyword-in-vueland\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F05\u002F12\u002Fthe-mysterious-this-keyword-in-vueland\u002Fsummary.json",[27,61,62],{},[63,64],"img",{"alt":65,"src":66},"blog hero image","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1618488085\u002Fblog\u002Fthe_misterios_this_keyword_in_vueland",[27,68,69,70,72,73,75,76,78,79,82],{},"If you are new to Vue.JS you might have realised that ",[22,71,24],{}," keyword is used everywhere. Some people might think that ",[22,74,24],{}," keyword is part of the Vue.JS framework itself. Others may wonder why ",[22,77,24],{}," keyword behaves a little differently in JavaScript, as compared to other languages such as Java, C++, PHP, etc. If you've experience an error that reads ",[22,80,81],{},"this is undefined",", you are not alone. In this article we will take a closer look at this common problem in Vue.JS, and how to solve it.",[27,84,85,86,88],{},"Before we dive into Vue.JS, we need to understand what exactly is ",[22,87,24],{}," keyword.",[27,90,91,93,94,96,97,99,100,102],{},[22,92,24],{}," has been part of the JavaScript language for a very long time. To give you a time reference, Internet Explorer version 4.0 had fully supported ",[22,95,24],{}," keyword. Inspite of it being with us for a very long period of time, people still struggle to understand it. So how do we define ",[22,98,24],{},"? In most cases, the value of ",[22,101,24],{}," is determined by how a function is called in JavaScript.",[104,105,107],"h2",{"id":106},"the-two-types-of-functions-in-javascript","The two types of functions in JavaScript",[27,109,110,111,114,115,118,119,121],{},"In Javascript we have two different types of functions, ",[42,112,113],{},"regular functions"," and ",[42,116,117],{},"arrow functions",". They operate in almost the same manner, with one exception — they treat ",[22,120,24],{}," variable differently.",[123,124,126],"h3",{"id":125},"example-of-a-regular-function","Example of a Regular function",[128,129,134],"pre",{"className":130,"code":131,"language":132,"meta":133,"style":133},"language-js shiki shiki-themes github-light github-dark","var sum = function (a, b) {\n    return a + b;\n};\n","js","",[22,135,136,171,186],{"__ignoreMap":133},[137,138,141,145,149,152,155,159,162,165,168],"span",{"class":139,"line":140},"line",1,[137,142,144],{"class":143},"szBVR","var",[137,146,148],{"class":147},"sScJk"," sum",[137,150,151],{"class":143}," =",[137,153,154],{"class":143}," function",[137,156,158],{"class":157},"sVt8B"," (",[137,160,45],{"class":161},"s4XuR",[137,163,164],{"class":157},", ",[137,166,167],{"class":161},"b",[137,169,170],{"class":157},") {\n",[137,172,174,177,180,183],{"class":139,"line":173},2,[137,175,176],{"class":143},"    return",[137,178,179],{"class":157}," a ",[137,181,182],{"class":143},"+",[137,184,185],{"class":157}," b;\n",[137,187,189],{"class":139,"line":188},3,[137,190,191],{"class":157},"};\n",[123,193,195],{"id":194},"example-of-an-arrow-function","Example of an Arrow function",[128,197,199],{"className":130,"code":198,"language":132,"meta":133,"style":133},"var sum = (a, b) => a + b;\n",[22,200,201],{"__ignoreMap":133},[137,202,203,205,207,209,211,213,215,217,220,223,225,227],{"class":139,"line":140},[137,204,144],{"class":143},[137,206,148],{"class":147},[137,208,151],{"class":143},[137,210,158],{"class":157},[137,212,45],{"class":161},[137,214,164],{"class":157},[137,216,167],{"class":161},[137,218,219],{"class":157},") ",[137,221,222],{"class":143},"=>",[137,224,179],{"class":157},[137,226,182],{"class":143},[137,228,185],{"class":157},[27,230,231,232,234],{},"Even though both functions above do exactly the same job, a difference is noted when we start using ",[22,233,24],{}," keyword in the function.",[123,236,238],{"id":237},"regular-function","Regular function",[128,240,242],{"className":130,"code":241,"language":132,"meta":133,"style":133},"var person = {\n    firstName: “Aleks”,\n    lastName: “Trpkovski”\n    thisInRegularFunction() {\n        console.log(\"My full name is \" + this.firstName + “ “ + this.lastName);\n    }\n};\nperson.thisInRegularFunction(); \u002F\u002F Output: My full name is Aleks Trpkovski\n",[22,243,244,257,262,267,276,289,295,300],{"__ignoreMap":133},[137,245,246,248,251,254],{"class":139,"line":140},[137,247,144],{"class":143},[137,249,250],{"class":157}," person ",[137,252,253],{"class":143},"=",[137,255,256],{"class":157}," {\n",[137,258,259],{"class":139,"line":173},[137,260,261],{"class":157},"    firstName: “Aleks”,\n",[137,263,264],{"class":139,"line":188},[137,265,266],{"class":157},"    lastName: “Trpkovski”\n",[137,268,270,273],{"class":139,"line":269},4,[137,271,272],{"class":147},"    thisInRegularFunction",[137,274,275],{"class":157},"() {\n",[137,277,279,282,286],{"class":139,"line":278},5,[137,280,281],{"class":157},"        console.log(",[137,283,285],{"class":284},"sZZnC","\"My full name is \"",[137,287,288],{"class":157}," + this.firstName + “ “ + this.lastName);\n",[137,290,292],{"class":139,"line":291},6,[137,293,294],{"class":157},"    }\n",[137,296,298],{"class":139,"line":297},7,[137,299,191],{"class":157},[137,301,303,306],{"class":139,"line":302},8,[137,304,305],{"class":157},"person.thisInRegularFunction(); ",[137,307,309],{"class":308},"sJ8bj","\u002F\u002F Output: My full name is Aleks Trpkovski\n",[123,311,313],{"id":312},"arrow-function","Arrow function",[128,315,317],{"className":130,"code":316,"language":132,"meta":133,"style":133},"var person = {\n    firstName: “Aleks”,\n    lastName: “Trpkovski”\n    thisInArrowFunction:() => {\n        console.log(\"My full name is \" + this.firstName + “ “ + this.lastName);\n    }\n};\nperson.thisInArrowFunction; \u002F\u002F Output: My full name is\n",[22,318,319,329,333,337,346,381,385,389],{"__ignoreMap":133},[137,320,321,323,325,327],{"class":139,"line":140},[137,322,144],{"class":143},[137,324,250],{"class":157},[137,326,253],{"class":143},[137,328,256],{"class":157},[137,330,331],{"class":139,"line":173},[137,332,261],{"class":157},[137,334,335],{"class":139,"line":188},[137,336,266],{"class":157},[137,338,339,342,344],{"class":139,"line":269},[137,340,341],{"class":157},"    thisInArrowFunction:() ",[137,343,222],{"class":143},[137,345,256],{"class":157},[137,347,348,351,354,357,359,362,366,369,371,374,376,378],{"class":139,"line":278},[137,349,350],{"class":157},"        console.",[137,352,353],{"class":147},"log",[137,355,356],{"class":157},"(",[137,358,285],{"class":284},[137,360,361],{"class":143}," +",[137,363,365],{"class":364},"sj4cs"," this",[137,367,368],{"class":157},".firstName ",[137,370,182],{"class":143},[137,372,373],{"class":157}," “ “ ",[137,375,182],{"class":143},[137,377,365],{"class":364},[137,379,380],{"class":157},".lastName);\n",[137,382,383],{"class":139,"line":291},[137,384,294],{"class":157},[137,386,387],{"class":139,"line":297},[137,388,191],{"class":157},[137,390,391,394],{"class":139,"line":302},[137,392,393],{"class":157},"person.thisInArrowFunction; ",[137,395,396],{"class":308},"\u002F\u002F Output: My full name is\n",[27,398,399,400,402,403,405],{},"From the examples above, this demonstrates the point of arrow functions not having their own ",[22,401,24],{},". In order to fully understand the behaviour of ",[22,404,24],{}," keyword in both regular and arrow functions, please review the following reasoning.",[104,407,409,411],{"id":408},"this-in-regular-functions",[22,410,24],{}," in Regular functions",[27,413,414,416,417,419,420,422],{},[22,415,24],{}," in regular JavaScript functions refers to the object that the function belongs to. In other words the value of ",[22,418,24],{}," depends on how the function is called, not where the function was declared. Even though the function was declared in a specific file or a particular object, ",[22,421,24],{}," changes its value based on the owner to the function call. It is important to remember the same function can have different owners in different scenarios.",[27,424,425,426,428],{},"The value of ",[22,427,24],{}," in regular JavaScript function is determined by 4 rules.",[104,430,432],{"id":431},"_1-default-binding","1. Default Binding",[27,434,435,436,438,439,441],{},"Default Binding happens when a function is invoked without any of these other 3 rules. In this rule ",[22,437,24],{}," points to the global object. This means that if you are in the browser, ",[22,440,24],{}," will be the window object. Default binding is applied for standalone functions. For example, any functions that are called without a “.” before it.",[128,443,445],{"className":130,"code":444,"language":132,"meta":133,"style":133},"obj.foo(); \u002F\u002F Not Standalone function\nfoo(); \u002F\u002F Standalone function, Default Binding applies\n",[22,446,447,461],{"__ignoreMap":133},[137,448,449,452,455,458],{"class":139,"line":140},[137,450,451],{"class":157},"obj.",[137,453,454],{"class":147},"foo",[137,456,457],{"class":157},"(); ",[137,459,460],{"class":308},"\u002F\u002F Not Standalone function\n",[137,462,463,465,467],{"class":139,"line":173},[137,464,454],{"class":147},[137,466,457],{"class":157},[137,468,469],{"class":308},"\u002F\u002F Standalone function, Default Binding applies\n",[123,471,473],{"id":472},"examples-for-default-binding","Examples for Default Binding",[128,475,477],{"className":130,"code":476,"language":132,"meta":133,"style":133},"function foo() {\n    console.log(“My name is “ + this.name);\n}\n\nvar name = “Aleks”;\n\nfoo(); \u002F\u002F Output: My name is Aleks.\n",[22,478,479,489,506,511,517,529,533],{"__ignoreMap":133},[137,480,481,484,487],{"class":139,"line":140},[137,482,483],{"class":143},"function",[137,485,486],{"class":147}," foo",[137,488,275],{"class":157},[137,490,491,494,496,499,501,503],{"class":139,"line":173},[137,492,493],{"class":157},"    console.",[137,495,353],{"class":147},[137,497,498],{"class":157},"(“My name is “ ",[137,500,182],{"class":143},[137,502,365],{"class":364},[137,504,505],{"class":157},".name);\n",[137,507,508],{"class":139,"line":188},[137,509,510],{"class":157},"}\n",[137,512,513],{"class":139,"line":269},[137,514,516],{"emptyLinePlaceholder":515},true,"\n",[137,518,519,521,524,526],{"class":139,"line":278},[137,520,144],{"class":143},[137,522,523],{"class":157}," name ",[137,525,253],{"class":143},[137,527,528],{"class":157}," “Aleks”;\n",[137,530,531],{"class":139,"line":291},[137,532,516],{"emptyLinePlaceholder":515},[137,534,535,537,539],{"class":139,"line":297},[137,536,454],{"class":147},[137,538,457],{"class":157},[137,540,541],{"class":308},"\u002F\u002F Output: My name is Aleks.\n",[104,543,545],{"id":544},"_2-implicit-binding","2. Implicit Binding",[27,547,548,549,552,553,555],{},"Implicit Binding happens when invoked with a ”.” before it. For instance, ",[22,550,551],{},"obj.foo()",". In this rule, whatever is to the left of the dot becomes the context for ",[22,554,24],{}," in the function.",[128,557,559],{"className":130,"code":558,"language":132,"meta":133,"style":133},"obj.foo(); \u002F\u002F the value of `this` in foo is obj\nobj1.obj2.foo(); \u002F\u002F the value of `this` in foo is obj2\nobj1.obj2.obj3.foo(); \u002F\u002F the value of `this` in foo is obj3\n",[22,560,561,572,584],{"__ignoreMap":133},[137,562,563,565,567,569],{"class":139,"line":140},[137,564,451],{"class":157},[137,566,454],{"class":147},[137,568,457],{"class":157},[137,570,571],{"class":308},"\u002F\u002F the value of `this` in foo is obj\n",[137,573,574,577,579,581],{"class":139,"line":173},[137,575,576],{"class":157},"obj1.obj2.",[137,578,454],{"class":147},[137,580,457],{"class":157},[137,582,583],{"class":308},"\u002F\u002F the value of `this` in foo is obj2\n",[137,585,586,589,591,593],{"class":139,"line":188},[137,587,588],{"class":157},"obj1.obj2.obj3.",[137,590,454],{"class":147},[137,592,457],{"class":157},[137,594,595],{"class":308},"\u002F\u002F the value of `this` in foo is obj3\n",[123,597,599],{"id":598},"examples-for-implicit-binding","Examples for Implicit Binding",[128,601,603],{"className":130,"code":602,"language":132,"meta":133,"style":133},"function foo() {\n    console.log(“My name is “ + this.name);\n}\n\nvar obj = {\n    name: “Aleks”,\n    foo: foo\n};\n\nobj.foo(); \u002F\u002F Output: My name is Aleks.\n",[22,604,605,613,627,631,635,646,651,656,660,665],{"__ignoreMap":133},[137,606,607,609,611],{"class":139,"line":140},[137,608,483],{"class":143},[137,610,486],{"class":147},[137,612,275],{"class":157},[137,614,615,617,619,621,623,625],{"class":139,"line":173},[137,616,493],{"class":157},[137,618,353],{"class":147},[137,620,498],{"class":157},[137,622,182],{"class":143},[137,624,365],{"class":364},[137,626,505],{"class":157},[137,628,629],{"class":139,"line":188},[137,630,510],{"class":157},[137,632,633],{"class":139,"line":269},[137,634,516],{"emptyLinePlaceholder":515},[137,636,637,639,642,644],{"class":139,"line":278},[137,638,144],{"class":143},[137,640,641],{"class":157}," obj ",[137,643,253],{"class":143},[137,645,256],{"class":157},[137,647,648],{"class":139,"line":291},[137,649,650],{"class":157},"    name: “Aleks”,\n",[137,652,653],{"class":139,"line":297},[137,654,655],{"class":157},"    foo: foo\n",[137,657,658],{"class":139,"line":302},[137,659,191],{"class":157},[137,661,663],{"class":139,"line":662},9,[137,664,516],{"emptyLinePlaceholder":515},[137,666,668,670,672,674],{"class":139,"line":667},10,[137,669,451],{"class":157},[137,671,454],{"class":147},[137,673,457],{"class":157},[137,675,541],{"class":308},[27,677,678,679,682,683,685,686,689],{},"One of the common frustrations you would face when ",[42,680,681],{},"Implicitly Binding"," a function, is that in certain circumstances, the function loses ",[22,684,24],{}," binding. This means that it will usually fall back to the ",[42,687,688],{},"Default Binding",". This often happens with nested functions or when creating a reference to the function to a new variable.",[104,691,693],{"id":692},"_21-implicit-binding-with-nested-functions","2.1. Implicit Binding with Nested Functions",[27,695,696,697,699],{},"When a function is nested inside a method of an object, ",[22,698,24],{}," variable depends on the inner function invocation.",[128,701,703],{"className":130,"code":702,"language":132,"meta":133,"style":133},"var obj = {\n    name: “Aleks”,\n    outer: function() {\n            function inner() {\n                console.log(“My name is “ + this.name);\n            }\n            inner(); \u002F\u002F Default Binding applies\n    },\n};\n\nvar name = “Nicole”;\n\nobj.outer(); \u002F\u002F Output: My name is Nicole\n\n",[22,704,705,715,719,731,741,756,761,771,776,780,784,796,801],{"__ignoreMap":133},[137,706,707,709,711,713],{"class":139,"line":140},[137,708,144],{"class":143},[137,710,641],{"class":157},[137,712,253],{"class":143},[137,714,256],{"class":157},[137,716,717],{"class":139,"line":173},[137,718,650],{"class":157},[137,720,721,724,727,729],{"class":139,"line":188},[137,722,723],{"class":147},"    outer",[137,725,726],{"class":157},": ",[137,728,483],{"class":143},[137,730,275],{"class":157},[137,732,733,736,739],{"class":139,"line":269},[137,734,735],{"class":143},"            function",[137,737,738],{"class":147}," inner",[137,740,275],{"class":157},[137,742,743,746,748,750,752,754],{"class":139,"line":278},[137,744,745],{"class":157},"                console.",[137,747,353],{"class":147},[137,749,498],{"class":157},[137,751,182],{"class":143},[137,753,365],{"class":364},[137,755,505],{"class":157},[137,757,758],{"class":139,"line":291},[137,759,760],{"class":157},"            }\n",[137,762,763,766,768],{"class":139,"line":297},[137,764,765],{"class":147},"            inner",[137,767,457],{"class":157},[137,769,770],{"class":308},"\u002F\u002F Default Binding applies\n",[137,772,773],{"class":139,"line":302},[137,774,775],{"class":157},"    },\n",[137,777,778],{"class":139,"line":662},[137,779,191],{"class":157},[137,781,782],{"class":139,"line":667},[137,783,516],{"emptyLinePlaceholder":515},[137,785,787,789,791,793],{"class":139,"line":786},11,[137,788,144],{"class":143},[137,790,523],{"class":157},[137,792,253],{"class":143},[137,794,795],{"class":157}," “Nicole”;\n",[137,797,799],{"class":139,"line":798},12,[137,800,516],{"emptyLinePlaceholder":515},[137,802,804,806,809,811],{"class":139,"line":803},13,[137,805,451],{"class":157},[137,807,808],{"class":147},"outer",[137,810,457],{"class":157},[137,812,813],{"class":308},"\u002F\u002F Output: My name is Nicole\n",[27,815,816,817,819],{},"In the example above, although the outer function was called using implicit binding, the inner function was called using default binding. Thus, ",[22,818,24],{}," points to the global object.",[104,821,823],{"id":822},"_22-implicit-binding-with-a-reference-to-the-function","2.2. Implicit Binding with a reference to the function",[128,825,827],{"className":130,"code":826,"language":132,"meta":133,"style":133},"function foo() {\n    console.log(“My name is “ + this.name);\n}\n\nvar obj = {\n    name: “Aleks”,\n    foo: foo\n};\n\nvar name: “Nicole”;\n\nvar bar = obj.foo();\n\nbar(); \u002F\u002F Output: My name is Nicole.\n",[22,828,829,837,851,855,859,869,873,877,881,885,904,908,925,929],{"__ignoreMap":133},[137,830,831,833,835],{"class":139,"line":140},[137,832,483],{"class":143},[137,834,486],{"class":147},[137,836,275],{"class":157},[137,838,839,841,843,845,847,849],{"class":139,"line":173},[137,840,493],{"class":157},[137,842,353],{"class":147},[137,844,498],{"class":157},[137,846,182],{"class":143},[137,848,365],{"class":364},[137,850,505],{"class":157},[137,852,853],{"class":139,"line":188},[137,854,510],{"class":157},[137,856,857],{"class":139,"line":269},[137,858,516],{"emptyLinePlaceholder":515},[137,860,861,863,865,867],{"class":139,"line":278},[137,862,144],{"class":143},[137,864,641],{"class":157},[137,866,253],{"class":143},[137,868,256],{"class":157},[137,870,871],{"class":139,"line":291},[137,872,650],{"class":157},[137,874,875],{"class":139,"line":297},[137,876,655],{"class":157},[137,878,879],{"class":139,"line":302},[137,880,191],{"class":157},[137,882,883],{"class":139,"line":662},[137,884,516],{"emptyLinePlaceholder":515},[137,886,887,889,892,895,898,901],{"class":139,"line":667},[137,888,144],{"class":143},[137,890,891],{"class":157}," name",[137,893,894],{"class":143},":",[137,896,897],{"class":157}," “",[137,899,900],{"class":147},"Nicole",[137,902,903],{"class":157},"”;\n",[137,905,906],{"class":139,"line":786},[137,907,516],{"emptyLinePlaceholder":515},[137,909,910,912,915,917,920,922],{"class":139,"line":798},[137,911,144],{"class":143},[137,913,914],{"class":157}," bar ",[137,916,253],{"class":143},[137,918,919],{"class":157}," obj.",[137,921,454],{"class":147},[137,923,924],{"class":157},"();\n",[137,926,927],{"class":139,"line":803},[137,928,516],{"emptyLinePlaceholder":515},[137,930,932,935,937],{"class":139,"line":931},14,[137,933,934],{"class":147},"bar",[137,936,457],{"class":157},[137,938,939],{"class":308},"\u002F\u002F Output: My name is Nicole.\n",[27,941,942,943,164,945,948,949,952],{},"Although bar appears to be reference to ",[22,944,551],{},[22,946,947],{},"bar()"," directly refers to ",[22,950,951],{},"foo()",". Hence, default binding applies.",[104,954,956],{"id":955},"_3-explicit-binding","3. Explicit Binding",[27,958,959,960,962,963,966,967,970,971,974,975,977,978,981,982,984,985,987,988,991,992,164,995,114,998,1001],{},"Explicit binding of ",[22,961,24],{}," happens when one of the three ",[22,964,965],{},".call()",", .",[22,968,969],{},"apply()",", or .",[22,972,973],{},"bind()"," are used in a function. In that way we can force a function to use a certain object as ",[22,976,24],{},". For instance, when calling ",[22,979,980],{},"foo.call(obj)",", the value of ",[22,983,24],{}," in the function ",[22,986,454],{}," becomes ",[22,989,990],{},"obj",". ",[42,993,994],{},"Call",[42,996,997],{},"apply",[42,999,1000],{},"bind"," do the same thing, with some little differences.",[1003,1004,1005,1018],"ul",{},[1006,1007,1008,1010,1011,1013,1014,1017],"li",{},[22,1009,965],{},": Pass in the required object (value of ",[22,1012,24],{},") as the first parameter, along with additional parameters that are separated by comma. For example ",[22,1015,1016],{},"foo.call(obj, param1, param2, …)",".",[1006,1019,1020,1023,1024,1026,1027,164,1029,1032,1033,1017],{},[22,1021,1022],{},".apply",": Is almost the same as ",[22,1025,965],{}," with only difference in the way the actual parameters are passed. Unlikely ",[42,1028,994],{},[42,1030,1031],{},"Apply"," accepts parameters as an array. For example ",[22,1034,1035],{},"foo.apply(obj, [param1, param2, …])",[128,1037,1039],{"className":130,"code":1038,"language":132,"meta":133,"style":133},"function foo(age, city) {\n    console.log(“My full name is “ + this.firstName + “ ” + this.lastName + “ ” + age + “ years old, living in ” + city);\n }\n\nvar obj = {\n    firstName: “Aleks”,\n    lastName: “Trpkovski”\n}\n\nvar age= 32, city = “Melbourne”;\n\nfoo.call(obj, age, city); \u002F\u002F Output: My full name is Aleks Trpkovski 32 years old, living in Melbourne\nfoo.apply(obj, [age, city]); \u002F\u002F Output: My full name is Aleks Trpkovski 32 years old, living in Melbourne\n",[22,1040,1041,1059,1111,1116,1120,1130,1134,1138,1142,1146,1166,1170,1184],{"__ignoreMap":133},[137,1042,1043,1045,1047,1049,1052,1054,1057],{"class":139,"line":140},[137,1044,483],{"class":143},[137,1046,486],{"class":147},[137,1048,356],{"class":157},[137,1050,1051],{"class":161},"age",[137,1053,164],{"class":157},[137,1055,1056],{"class":161},"city",[137,1058,170],{"class":157},[137,1060,1061,1063,1065,1068,1070,1072,1074,1076,1079,1081,1083,1086,1088,1090,1092,1095,1097,1100,1103,1106,1108],{"class":139,"line":173},[137,1062,493],{"class":157},[137,1064,353],{"class":147},[137,1066,1067],{"class":157},"(“My full name is “ ",[137,1069,182],{"class":143},[137,1071,365],{"class":364},[137,1073,368],{"class":157},[137,1075,182],{"class":143},[137,1077,1078],{"class":157}," “ ” ",[137,1080,182],{"class":143},[137,1082,365],{"class":364},[137,1084,1085],{"class":157},".lastName ",[137,1087,182],{"class":143},[137,1089,1078],{"class":157},[137,1091,182],{"class":143},[137,1093,1094],{"class":157}," age ",[137,1096,182],{"class":143},[137,1098,1099],{"class":157}," “ years old, living ",[137,1101,1102],{"class":143},"in",[137,1104,1105],{"class":157}," ” ",[137,1107,182],{"class":143},[137,1109,1110],{"class":157}," city);\n",[137,1112,1113],{"class":139,"line":188},[137,1114,1115],{"class":157}," }\n",[137,1117,1118],{"class":139,"line":269},[137,1119,516],{"emptyLinePlaceholder":515},[137,1121,1122,1124,1126,1128],{"class":139,"line":278},[137,1123,144],{"class":143},[137,1125,641],{"class":157},[137,1127,253],{"class":143},[137,1129,256],{"class":157},[137,1131,1132],{"class":139,"line":291},[137,1133,261],{"class":157},[137,1135,1136],{"class":139,"line":297},[137,1137,266],{"class":157},[137,1139,1140],{"class":139,"line":302},[137,1141,510],{"class":157},[137,1143,1144],{"class":139,"line":662},[137,1145,516],{"emptyLinePlaceholder":515},[137,1147,1148,1150,1153,1155,1158,1161,1163],{"class":139,"line":667},[137,1149,144],{"class":143},[137,1151,1152],{"class":157}," age",[137,1154,253],{"class":143},[137,1156,1157],{"class":364}," 32",[137,1159,1160],{"class":157},", city ",[137,1162,253],{"class":143},[137,1164,1165],{"class":157}," “Melbourne”;\n",[137,1167,1168],{"class":139,"line":786},[137,1169,516],{"emptyLinePlaceholder":515},[137,1171,1172,1175,1178,1181],{"class":139,"line":798},[137,1173,1174],{"class":157},"foo.",[137,1176,1177],{"class":147},"call",[137,1179,1180],{"class":157},"(obj, age, city); ",[137,1182,1183],{"class":308},"\u002F\u002F Output: My full name is Aleks Trpkovski 32 years old, living in Melbourne\n",[137,1185,1186,1188,1190,1193],{"class":139,"line":803},[137,1187,1174],{"class":157},[137,1189,997],{"class":147},[137,1191,1192],{"class":157},"(obj, [age, city]); ",[137,1194,1183],{"class":308},[1003,1196,1197],{},[1006,1198,1199,1202,1203,114,1205,1207,1208,1211],{},[22,1200,1201],{},".bind()",": Is a little bit different than ",[42,1204,994],{},[42,1206,1031],{},". When call a function with ",[42,1209,1210],{},"Bind",", returns new function of the same name.",[128,1213,1215],{"className":130,"code":1214,"language":132,"meta":133,"style":133},"function foo(age, city) {\n    console.log(“My full name is “ + this.firstName + “ ” + this.lastName + “ ” + age + “ years old, living in ” + city);\n }\n\nvar obj = {\n    firstName: “Aleks”,\n    lastName: “Trpkovski”\n}\n\nvar age= 32, city = “Melbourne”;\n\nvar bar = foo.bind(obj, age, city)\nbar(); \u002F\u002F Output: My full name is Aleks Trpkovski 32 years old, living in Melbourne\n",[22,1216,1217,1233,1277,1281,1285,1295,1299,1303,1307,1311,1327,1331,1347],{"__ignoreMap":133},[137,1218,1219,1221,1223,1225,1227,1229,1231],{"class":139,"line":140},[137,1220,483],{"class":143},[137,1222,486],{"class":147},[137,1224,356],{"class":157},[137,1226,1051],{"class":161},[137,1228,164],{"class":157},[137,1230,1056],{"class":161},[137,1232,170],{"class":157},[137,1234,1235,1237,1239,1241,1243,1245,1247,1249,1251,1253,1255,1257,1259,1261,1263,1265,1267,1269,1271,1273,1275],{"class":139,"line":173},[137,1236,493],{"class":157},[137,1238,353],{"class":147},[137,1240,1067],{"class":157},[137,1242,182],{"class":143},[137,1244,365],{"class":364},[137,1246,368],{"class":157},[137,1248,182],{"class":143},[137,1250,1078],{"class":157},[137,1252,182],{"class":143},[137,1254,365],{"class":364},[137,1256,1085],{"class":157},[137,1258,182],{"class":143},[137,1260,1078],{"class":157},[137,1262,182],{"class":143},[137,1264,1094],{"class":157},[137,1266,182],{"class":143},[137,1268,1099],{"class":157},[137,1270,1102],{"class":143},[137,1272,1105],{"class":157},[137,1274,182],{"class":143},[137,1276,1110],{"class":157},[137,1278,1279],{"class":139,"line":188},[137,1280,1115],{"class":157},[137,1282,1283],{"class":139,"line":269},[137,1284,516],{"emptyLinePlaceholder":515},[137,1286,1287,1289,1291,1293],{"class":139,"line":278},[137,1288,144],{"class":143},[137,1290,641],{"class":157},[137,1292,253],{"class":143},[137,1294,256],{"class":157},[137,1296,1297],{"class":139,"line":291},[137,1298,261],{"class":157},[137,1300,1301],{"class":139,"line":297},[137,1302,266],{"class":157},[137,1304,1305],{"class":139,"line":302},[137,1306,510],{"class":157},[137,1308,1309],{"class":139,"line":662},[137,1310,516],{"emptyLinePlaceholder":515},[137,1312,1313,1315,1317,1319,1321,1323,1325],{"class":139,"line":667},[137,1314,144],{"class":143},[137,1316,1152],{"class":157},[137,1318,253],{"class":143},[137,1320,1157],{"class":364},[137,1322,1160],{"class":157},[137,1324,253],{"class":143},[137,1326,1165],{"class":157},[137,1328,1329],{"class":139,"line":786},[137,1330,516],{"emptyLinePlaceholder":515},[137,1332,1333,1335,1337,1339,1342,1344],{"class":139,"line":798},[137,1334,144],{"class":143},[137,1336,914],{"class":157},[137,1338,253],{"class":143},[137,1340,1341],{"class":157}," foo.",[137,1343,1000],{"class":147},[137,1345,1346],{"class":157},"(obj, age, city)\n",[137,1348,1349,1351,1353],{"class":139,"line":803},[137,1350,934],{"class":147},[137,1352,457],{"class":157},[137,1354,1183],{"class":308},[104,1356,1358,1359,1362],{"id":1357},"_4-new-binding","4. ",[22,1360,1361],{},"new"," Binding",[27,1364,1365,1366,1368,1369,1371],{},"When a function is invoked using the ",[22,1367,1361],{}," operator, also known as a constructor call, we create a brand new empty object and set that new object as ",[22,1370,24],{}," inside the function.",[128,1373,1375],{"className":130,"code":1374,"language":132,"meta":133,"style":133},"function foo(name) {\n    this.name = “My name is “ + name;\n}\n\nvar bar = new foo(“Aleks”);\nconsole.log(bar.name); \u002F\u002F Output: My name is Aleks\n",[22,1376,1377,1390,1408,1412,1416,1432],{"__ignoreMap":133},[137,1378,1379,1381,1383,1385,1388],{"class":139,"line":140},[137,1380,483],{"class":143},[137,1382,486],{"class":147},[137,1384,356],{"class":157},[137,1386,1387],{"class":161},"name",[137,1389,170],{"class":157},[137,1391,1392,1395,1398,1400,1403,1405],{"class":139,"line":173},[137,1393,1394],{"class":364},"    this",[137,1396,1397],{"class":157},".name ",[137,1399,253],{"class":143},[137,1401,1402],{"class":157}," “My name is “ ",[137,1404,182],{"class":143},[137,1406,1407],{"class":157}," name;\n",[137,1409,1410],{"class":139,"line":188},[137,1411,510],{"class":157},[137,1413,1414],{"class":139,"line":269},[137,1415,516],{"emptyLinePlaceholder":515},[137,1417,1418,1420,1422,1424,1427,1429],{"class":139,"line":278},[137,1419,144],{"class":143},[137,1421,914],{"class":157},[137,1423,253],{"class":143},[137,1425,1426],{"class":143}," new",[137,1428,486],{"class":147},[137,1430,1431],{"class":157},"(“Aleks”);\n",[137,1433,1434,1437,1439,1442],{"class":139,"line":291},[137,1435,1436],{"class":157},"console.",[137,1438,353],{"class":147},[137,1440,1441],{"class":157},"(bar.name); ",[137,1443,1444],{"class":308},"\u002F\u002F Output: My name is Aleks\n",[104,1446,1448],{"id":1447},"arrow-functions","Arrow functions",[27,1450,1451,1452,1454,1455,1457],{},"ES6 introduced a special kind of function known as arrow functions. Unlike the regular functions where the value of ",[22,1453,24],{}," is determined by the 4 rules mentioned above, the arrow functions use ",[22,1456,24],{}," from the outer function or the global scope in which it is declared. For example, if the outer function is an arrow function, then it checks for the next outer function and repeats till the global scope.",[128,1459,1461],{"className":130,"code":1460,"language":132,"meta":133,"style":133},"function foo(){\n    var bar = () => {\n            console.log(this);\n        };\n    bar();\n}\n\nvar obj1 = {\n    name: “Aleks”,\n    foo: foo\n};\n\nvar obj2 = {\n    name: “”Nicole\n};\n\nfoo(); \u002F\u002F Output: Window {}\nobj1.foo(); \u002F\u002F {name: “Aleks”, foo: ƒ}\nfoo.call(obj2); \u002F\u002F {name: “Nicole”}\n",[22,1462,1463,1472,1489,1503,1508,1515,1519,1523,1534,1538,1542,1546,1550,1561,1566,1571,1576,1586,1599],{"__ignoreMap":133},[137,1464,1465,1467,1469],{"class":139,"line":140},[137,1466,483],{"class":143},[137,1468,486],{"class":147},[137,1470,1471],{"class":157},"(){\n",[137,1473,1474,1477,1480,1482,1485,1487],{"class":139,"line":173},[137,1475,1476],{"class":143},"    var",[137,1478,1479],{"class":147}," bar",[137,1481,151],{"class":143},[137,1483,1484],{"class":157}," () ",[137,1486,222],{"class":143},[137,1488,256],{"class":157},[137,1490,1491,1494,1496,1498,1500],{"class":139,"line":188},[137,1492,1493],{"class":157},"            console.",[137,1495,353],{"class":147},[137,1497,356],{"class":157},[137,1499,24],{"class":364},[137,1501,1502],{"class":157},");\n",[137,1504,1505],{"class":139,"line":269},[137,1506,1507],{"class":157},"        };\n",[137,1509,1510,1513],{"class":139,"line":278},[137,1511,1512],{"class":147},"    bar",[137,1514,924],{"class":157},[137,1516,1517],{"class":139,"line":291},[137,1518,510],{"class":157},[137,1520,1521],{"class":139,"line":297},[137,1522,516],{"emptyLinePlaceholder":515},[137,1524,1525,1527,1530,1532],{"class":139,"line":302},[137,1526,144],{"class":143},[137,1528,1529],{"class":157}," obj1 ",[137,1531,253],{"class":143},[137,1533,256],{"class":157},[137,1535,1536],{"class":139,"line":662},[137,1537,650],{"class":157},[137,1539,1540],{"class":139,"line":667},[137,1541,655],{"class":157},[137,1543,1544],{"class":139,"line":786},[137,1545,191],{"class":157},[137,1547,1548],{"class":139,"line":798},[137,1549,516],{"emptyLinePlaceholder":515},[137,1551,1552,1554,1557,1559],{"class":139,"line":803},[137,1553,144],{"class":143},[137,1555,1556],{"class":157}," obj2 ",[137,1558,253],{"class":143},[137,1560,256],{"class":157},[137,1562,1563],{"class":139,"line":931},[137,1564,1565],{"class":157},"    name: “”Nicole\n",[137,1567,1569],{"class":139,"line":1568},15,[137,1570,191],{"class":157},[137,1572,1574],{"class":139,"line":1573},16,[137,1575,516],{"emptyLinePlaceholder":515},[137,1577,1579,1581,1583],{"class":139,"line":1578},17,[137,1580,454],{"class":147},[137,1582,457],{"class":157},[137,1584,1585],{"class":308},"\u002F\u002F Output: Window {}\n",[137,1587,1589,1592,1594,1596],{"class":139,"line":1588},18,[137,1590,1591],{"class":157},"obj1.",[137,1593,454],{"class":147},[137,1595,457],{"class":157},[137,1597,1598],{"class":308},"\u002F\u002F {name: “Aleks”, foo: ƒ}\n",[137,1600,1602,1604,1606,1609],{"class":139,"line":1601},19,[137,1603,1174],{"class":157},[137,1605,1177],{"class":147},[137,1607,1608],{"class":157},"(obj2); ",[137,1610,1611],{"class":308},"\u002F\u002F {name: “Nicole”}\n",[27,1613,1614,1615,1617,1618,1620,1621,1017],{},"As we can see from the example above, each time ",[22,1616,934],{}," is called, the value of ",[22,1619,24],{}," is taken from the outer function. In this case, ",[22,1622,454],{},[128,1624,1626],{"className":130,"code":1625,"language":132,"meta":133,"style":133},"var foo = () => {\n    console.log(this);\n};\n\nvar obj1 = {\n    foo: foo,\n    bar: () => {\n        console.log(this);\n    },\n};\n\nvar obj2 = {};\n\nfoo(); \u002F\u002F Output: Window {}\nobj1.foo(); \u002F\u002F Output: Window {}\nobj1.bar(); \u002F\u002F Output: Window {}\nfoo.call(obj2); \u002F\u002F Output: Window {}\nvar obj3 = new foo(); \u002F\u002F Output: Window {}\n",[22,1627,1628,1642,1654,1658,1662,1672,1677,1688,1700,1704,1708,1712,1723,1727,1735,1745,1755,1765],{"__ignoreMap":133},[137,1629,1630,1632,1634,1636,1638,1640],{"class":139,"line":140},[137,1631,144],{"class":143},[137,1633,486],{"class":147},[137,1635,151],{"class":143},[137,1637,1484],{"class":157},[137,1639,222],{"class":143},[137,1641,256],{"class":157},[137,1643,1644,1646,1648,1650,1652],{"class":139,"line":173},[137,1645,493],{"class":157},[137,1647,353],{"class":147},[137,1649,356],{"class":157},[137,1651,24],{"class":364},[137,1653,1502],{"class":157},[137,1655,1656],{"class":139,"line":188},[137,1657,191],{"class":157},[137,1659,1660],{"class":139,"line":269},[137,1661,516],{"emptyLinePlaceholder":515},[137,1663,1664,1666,1668,1670],{"class":139,"line":278},[137,1665,144],{"class":143},[137,1667,1529],{"class":157},[137,1669,253],{"class":143},[137,1671,256],{"class":157},[137,1673,1674],{"class":139,"line":291},[137,1675,1676],{"class":157},"    foo: foo,\n",[137,1678,1679,1681,1684,1686],{"class":139,"line":297},[137,1680,1512],{"class":147},[137,1682,1683],{"class":157},": () ",[137,1685,222],{"class":143},[137,1687,256],{"class":157},[137,1689,1690,1692,1694,1696,1698],{"class":139,"line":302},[137,1691,350],{"class":157},[137,1693,353],{"class":147},[137,1695,356],{"class":157},[137,1697,24],{"class":364},[137,1699,1502],{"class":157},[137,1701,1702],{"class":139,"line":662},[137,1703,775],{"class":157},[137,1705,1706],{"class":139,"line":667},[137,1707,191],{"class":157},[137,1709,1710],{"class":139,"line":786},[137,1711,516],{"emptyLinePlaceholder":515},[137,1713,1714,1716,1718,1720],{"class":139,"line":798},[137,1715,144],{"class":143},[137,1717,1556],{"class":157},[137,1719,253],{"class":143},[137,1721,1722],{"class":157}," {};\n",[137,1724,1725],{"class":139,"line":803},[137,1726,516],{"emptyLinePlaceholder":515},[137,1728,1729,1731,1733],{"class":139,"line":931},[137,1730,454],{"class":147},[137,1732,457],{"class":157},[137,1734,1585],{"class":308},[137,1736,1737,1739,1741,1743],{"class":139,"line":1568},[137,1738,1591],{"class":157},[137,1740,454],{"class":147},[137,1742,457],{"class":157},[137,1744,1585],{"class":308},[137,1746,1747,1749,1751,1753],{"class":139,"line":1573},[137,1748,1591],{"class":157},[137,1750,934],{"class":147},[137,1752,457],{"class":157},[137,1754,1585],{"class":308},[137,1756,1757,1759,1761,1763],{"class":139,"line":1578},[137,1758,1174],{"class":157},[137,1760,1177],{"class":147},[137,1762,1608],{"class":157},[137,1764,1585],{"class":308},[137,1766,1767,1769,1772,1774,1776,1778,1780],{"class":139,"line":1588},[137,1768,144],{"class":143},[137,1770,1771],{"class":157}," obj3 ",[137,1773,253],{"class":143},[137,1775,1426],{"class":143},[137,1777,486],{"class":147},[137,1779,457],{"class":157},[137,1781,1585],{"class":308},[27,1783,1784],{},"As we can see in the last example above, none of the 4 binding rules has any direct impact on arrow functions.",[27,1786,1787,1788,1790],{},"Now when ",[22,1789,24],{}," make sense, it brings us to the next section — how all this applies to Vue.JS.",[104,1792,1794,1795,1797],{"id":1793},"understanding-this-keyword-in-vuejs","Understanding ",[22,1796,24],{}," keyword in Vue.JS",[27,1799,1800,1801,1803],{},"In this blog post, we won't focus on the fundamentals of Vue.JS framework. That would be going off track from the main purpose of this post. We will instead look at how ",[22,1802,24],{}," keyword affects the function declaration in the method property.",[27,1805,1806],{},"As seen previously, we have two types of functions in JavaScript. So technically we can use either regular or arrow to declare a function in Vue.JS.",[128,1808,1810],{"className":130,"code":1809,"language":132,"meta":133,"style":133},"methods: {\n    regularFunction() {\n            console.log(this); \u002F\u002F Output: Vue componet\n    }\n}\n",[22,1811,1812,1820,1827,1843,1847],{"__ignoreMap":133},[137,1813,1814,1817],{"class":139,"line":140},[137,1815,1816],{"class":147},"methods",[137,1818,1819],{"class":157},": {\n",[137,1821,1822,1825],{"class":139,"line":173},[137,1823,1824],{"class":147},"    regularFunction",[137,1826,275],{"class":157},[137,1828,1829,1831,1833,1835,1837,1840],{"class":139,"line":188},[137,1830,1493],{"class":157},[137,1832,353],{"class":147},[137,1834,356],{"class":157},[137,1836,24],{"class":364},[137,1838,1839],{"class":157},"); ",[137,1841,1842],{"class":308},"\u002F\u002F Output: Vue componet\n",[137,1844,1845],{"class":139,"line":269},[137,1846,294],{"class":157},[137,1848,1849],{"class":139,"line":278},[137,1850,510],{"class":157},[128,1852,1854],{"className":130,"code":1853,"language":132,"meta":133,"style":133},"methods: {\n    arrowFunction: () => {\n        console.log(this); \u002F\u002F Output: this is undefined\n    };\n}\n",[22,1855,1856,1862,1873,1888,1893],{"__ignoreMap":133},[137,1857,1858,1860],{"class":139,"line":140},[137,1859,1816],{"class":147},[137,1861,1819],{"class":157},[137,1863,1864,1867,1869,1871],{"class":139,"line":173},[137,1865,1866],{"class":147},"    arrowFunction",[137,1868,1683],{"class":157},[137,1870,222],{"class":143},[137,1872,256],{"class":157},[137,1874,1875,1877,1879,1881,1883,1885],{"class":139,"line":188},[137,1876,350],{"class":157},[137,1878,353],{"class":147},[137,1880,356],{"class":157},[137,1882,24],{"class":364},[137,1884,1839],{"class":157},[137,1886,1887],{"class":308},"\u002F\u002F Output: this is undefined\n",[137,1889,1890],{"class":139,"line":269},[137,1891,1892],{"class":157},"    };\n",[137,1894,1895],{"class":139,"line":278},[137,1896,510],{"class":157},[27,1898,1899,1900,1902],{},"Even though both function declarations are correct, there is a difference in how they treat ",[22,1901,24],{}," variable.",[27,1904,1905,1906,1908,1909,1911,1912,1914],{},"In a regular function, ",[22,1907,24],{}," refers to the owner of the function. In our case, ",[22,1910,24],{}," refers to the Vue component. Hence, we can safely use ",[22,1913,24],{}," to get any of the data properties, computed properties or methods of the Vue component.",[27,1916,1917,1918,1920,1921,1923,1924,1926],{},"On the other hand, in an arrow function, ",[22,1919,24],{}," does not refer to the Vue component. As explained previously, the arrow functions use ",[22,1922,24],{}," from the outer function or the global scope in which it is declared. In our example above that is the global scope and the reasoning behind of getting ",[22,1925,81],{}," printed in the console.",[27,1928,1929],{},"It is recommended to use a regular function with Vue, especially when creating methods, computed properties, watched properties. But in certain scenarios, arrow functions come in very handy as well. We will have a look at various issues and how to solve them with either regular or arrow functions in the next few examples below.",[27,1931,1932],{},"Consider this code:",[128,1934,1936],{"className":130,"code":1935,"language":132,"meta":133,"style":133},"data() {\n    return {\n            name: \"Aleks\",\n        }\n},\nmethods: {\n        foo() {\n        console.log(\"My name is: \" + this.name); \u002F\u002F Output: My name is Aleks.\n                                                 \u002F\u002F `this` on this line, refers to the Vue Component.\n                                                 \u002F\u002F we can use `this` to get any of the data properties of this Vue Component.\n\n            var bar = function() {\n                console.log(\"My name is: \" + this.name); \u002F\u002F Output: this is undefined\n            };                                           \u002F\u002F this here refers to the Window object.\n                                                         \u002F\u002F the Default Binding applies\n            setTimeout(bar(), 100);\n        },\n}\n",[22,1937,1938,1945,1951,1962,1967,1972,1978,1985,2005,2010,2015,2019,2032,2050,2058,2063,2080,2085],{"__ignoreMap":133},[137,1939,1940,1943],{"class":139,"line":140},[137,1941,1942],{"class":147},"data",[137,1944,275],{"class":157},[137,1946,1947,1949],{"class":139,"line":173},[137,1948,176],{"class":143},[137,1950,256],{"class":157},[137,1952,1953,1956,1959],{"class":139,"line":188},[137,1954,1955],{"class":157},"            name: ",[137,1957,1958],{"class":284},"\"Aleks\"",[137,1960,1961],{"class":157},",\n",[137,1963,1964],{"class":139,"line":269},[137,1965,1966],{"class":157},"        }\n",[137,1968,1969],{"class":139,"line":278},[137,1970,1971],{"class":157},"},\n",[137,1973,1974,1976],{"class":139,"line":291},[137,1975,1816],{"class":147},[137,1977,1819],{"class":157},[137,1979,1980,1983],{"class":139,"line":297},[137,1981,1982],{"class":147},"        foo",[137,1984,275],{"class":157},[137,1986,1987,1989,1991,1993,1996,1998,2000,2003],{"class":139,"line":302},[137,1988,350],{"class":157},[137,1990,353],{"class":147},[137,1992,356],{"class":157},[137,1994,1995],{"class":284},"\"My name is: \"",[137,1997,361],{"class":143},[137,1999,365],{"class":364},[137,2001,2002],{"class":157},".name); ",[137,2004,541],{"class":308},[137,2006,2007],{"class":139,"line":662},[137,2008,2009],{"class":308},"                                                 \u002F\u002F `this` on this line, refers to the Vue Component.\n",[137,2011,2012],{"class":139,"line":667},[137,2013,2014],{"class":308},"                                                 \u002F\u002F we can use `this` to get any of the data properties of this Vue Component.\n",[137,2016,2017],{"class":139,"line":786},[137,2018,516],{"emptyLinePlaceholder":515},[137,2020,2021,2024,2026,2028,2030],{"class":139,"line":798},[137,2022,2023],{"class":143},"            var",[137,2025,1479],{"class":147},[137,2027,151],{"class":143},[137,2029,154],{"class":143},[137,2031,275],{"class":157},[137,2033,2034,2036,2038,2040,2042,2044,2046,2048],{"class":139,"line":803},[137,2035,745],{"class":157},[137,2037,353],{"class":147},[137,2039,356],{"class":157},[137,2041,1995],{"class":284},[137,2043,361],{"class":143},[137,2045,365],{"class":364},[137,2047,2002],{"class":157},[137,2049,1887],{"class":308},[137,2051,2052,2055],{"class":139,"line":931},[137,2053,2054],{"class":157},"            };                                           ",[137,2056,2057],{"class":308},"\u002F\u002F this here refers to the Window object.\n",[137,2059,2060],{"class":139,"line":1568},[137,2061,2062],{"class":308},"                                                         \u002F\u002F the Default Binding applies\n",[137,2064,2065,2068,2070,2072,2075,2078],{"class":139,"line":1573},[137,2066,2067],{"class":147},"            setTimeout",[137,2069,356],{"class":157},[137,2071,934],{"class":147},[137,2073,2074],{"class":157},"(), ",[137,2076,2077],{"class":364},"100",[137,2079,1502],{"class":157},[137,2081,2082],{"class":139,"line":1578},[137,2083,2084],{"class":157},"        },\n",[137,2086,2087],{"class":139,"line":1588},[137,2088,510],{"class":157},[27,2090,2091,2092,2094,2095,2098,2099,1017],{},"Where most likely we will get into trouble using ",[22,2093,24],{},", is when we declare another function inside the current function, as shown in the previous example. We have explained this scenario in the ",[42,2096,2097],{},"Implicit Binding"," section under ",[42,2100,2101],{},"Implicit Binding with Nested Functions",[27,2103,2104,2105,2107,2108,2110,2111,2113,2114,2116,2117,2119],{},"This is a common problem in Vue.JS, especially when dealing with callbacks. ",[22,2106,24],{}," in the ",[22,2109,934],{}," refers to the global object (the Window). As explained in the previous section in JavaScript, when you declare a new regular function, that function has it's own ",[22,2112,24],{}," variable, which is different from the outer function, in our case, ",[22,2115,454],{}," in which it is declared. The confusing part in the example above is the ",[42,2118,688],{}," in the second function.",[27,2121,2122,2123,2126],{},"Lets refactor the provided built-in function ",[22,2124,2125],{},"setTimeout()"," in JavaScript.",[128,2128,2130],{"className":130,"code":2129,"language":132,"meta":133,"style":133},"function SetTimeout(bar(), delay) {\n    \u002F\u002F wait (somehow) for ‘delay’ milliseconds\n    bar(); \u002F\u002F Standalone function, Default Binding applies\n}\n",[22,2131,2132,2150,2155,2163],{"__ignoreMap":133},[137,2133,2134,2136,2139,2141,2143,2145,2148],{"class":139,"line":140},[137,2135,483],{"class":143},[137,2137,2138],{"class":147}," SetTimeout",[137,2140,356],{"class":157},[137,2142,934],{"class":161},[137,2144,2074],{"class":157},[137,2146,2147],{"class":147},"delay",[137,2149,170],{"class":157},[137,2151,2152],{"class":139,"line":173},[137,2153,2154],{"class":308},"    \u002F\u002F wait (somehow) for ‘delay’ milliseconds\n",[137,2156,2157,2159,2161],{"class":139,"line":188},[137,2158,1512],{"class":147},[137,2160,457],{"class":157},[137,2162,469],{"class":308},[137,2164,2165],{"class":139,"line":269},[137,2166,510],{"class":157},[123,2168,2170,2171],{"id":2169},"how-can-we-fix-this","How can we fix ",[22,2172,24],{},[27,2174,2175,2176,2178],{},"There are several ways to deal with ",[22,2177,24],{}," inside a function in Vue.JS.",[27,2180,2181,2182,2185,2186,966,2188,970,2190,2192],{},"One solution would be to use ",[42,2183,2184],{},"Explicit Binding"," with one of the three ",[22,2187,965],{},[22,2189,969],{},[22,2191,973],{}," methods on the function call.",[128,2194,2196],{"className":130,"code":2195,"language":132,"meta":133,"style":133},"setTimeout(bar.call(this), 100); \u002F\u002F Output: My name is Aleks.\nsetTimeout(bar.apply(this), 100); \u002F\u002F Output: My name is Aleks.\nsetTimeout(bar.bind(this), 100); \u002F\u002F Output: My name is Aleks.\n",[22,2197,2198,2221,2241],{"__ignoreMap":133},[137,2199,2200,2203,2206,2208,2210,2212,2215,2217,2219],{"class":139,"line":140},[137,2201,2202],{"class":147},"setTimeout",[137,2204,2205],{"class":157},"(bar.",[137,2207,1177],{"class":147},[137,2209,356],{"class":157},[137,2211,24],{"class":364},[137,2213,2214],{"class":157},"), ",[137,2216,2077],{"class":364},[137,2218,1839],{"class":157},[137,2220,541],{"class":308},[137,2222,2223,2225,2227,2229,2231,2233,2235,2237,2239],{"class":139,"line":173},[137,2224,2202],{"class":147},[137,2226,2205],{"class":157},[137,2228,997],{"class":147},[137,2230,356],{"class":157},[137,2232,24],{"class":364},[137,2234,2214],{"class":157},[137,2236,2077],{"class":364},[137,2238,1839],{"class":157},[137,2240,541],{"class":308},[137,2242,2243,2245,2247,2249,2251,2253,2255,2257,2259],{"class":139,"line":188},[137,2244,2202],{"class":147},[137,2246,2205],{"class":157},[137,2248,1000],{"class":147},[137,2250,356],{"class":157},[137,2252,24],{"class":364},[137,2254,2214],{"class":157},[137,2256,2077],{"class":364},[137,2258,1839],{"class":157},[137,2260,541],{"class":308},[27,2262,2263],{},"The other solution is most commonly used — by using a closure.",[128,2265,2267],{"className":130,"code":2266,"language":132,"meta":133,"style":133},"data() {\n    return {\n            name: \"Aleks\",\n        }\n},\nmethods: {\n        foo() {\n        var self = this; \u002F\u002F In the variable self we save a reference to the this\n\n        console.log(\"My name is: \" + this.name); \u002F\u002F Output: My name is Aleks.\n                                                 \u002F\u002F `this` on this line, refers to the Vue Component.\n\n            var bar = function() {\n                console.log(\"My name is: \" + self.name);  \u002F\u002F Output: My name is Aleks.\n            };                                            \u002F\u002F this here refers to the Window.\n                                                          \u002F\u002F we can use the reference we declared self which is refering to the Vue Component.\n            setTimeout(bar(), 100);\n        },\n}\n",[22,2268,2269,2275,2281,2289,2293,2297,2303,2309,2327,2331,2349,2353,2357,2369,2386,2394,2399,2413,2417],{"__ignoreMap":133},[137,2270,2271,2273],{"class":139,"line":140},[137,2272,1942],{"class":147},[137,2274,275],{"class":157},[137,2276,2277,2279],{"class":139,"line":173},[137,2278,176],{"class":143},[137,2280,256],{"class":157},[137,2282,2283,2285,2287],{"class":139,"line":188},[137,2284,1955],{"class":157},[137,2286,1958],{"class":284},[137,2288,1961],{"class":157},[137,2290,2291],{"class":139,"line":269},[137,2292,1966],{"class":157},[137,2294,2295],{"class":139,"line":278},[137,2296,1971],{"class":157},[137,2298,2299,2301],{"class":139,"line":291},[137,2300,1816],{"class":147},[137,2302,1819],{"class":157},[137,2304,2305,2307],{"class":139,"line":297},[137,2306,1982],{"class":147},[137,2308,275],{"class":157},[137,2310,2311,2314,2317,2319,2321,2324],{"class":139,"line":302},[137,2312,2313],{"class":143},"        var",[137,2315,2316],{"class":157}," self ",[137,2318,253],{"class":143},[137,2320,365],{"class":364},[137,2322,2323],{"class":157},"; ",[137,2325,2326],{"class":308},"\u002F\u002F In the variable self we save a reference to the this\n",[137,2328,2329],{"class":139,"line":662},[137,2330,516],{"emptyLinePlaceholder":515},[137,2332,2333,2335,2337,2339,2341,2343,2345,2347],{"class":139,"line":667},[137,2334,350],{"class":157},[137,2336,353],{"class":147},[137,2338,356],{"class":157},[137,2340,1995],{"class":284},[137,2342,361],{"class":143},[137,2344,365],{"class":364},[137,2346,2002],{"class":157},[137,2348,541],{"class":308},[137,2350,2351],{"class":139,"line":786},[137,2352,2009],{"class":308},[137,2354,2355],{"class":139,"line":798},[137,2356,516],{"emptyLinePlaceholder":515},[137,2358,2359,2361,2363,2365,2367],{"class":139,"line":803},[137,2360,2023],{"class":143},[137,2362,1479],{"class":147},[137,2364,151],{"class":143},[137,2366,154],{"class":143},[137,2368,275],{"class":157},[137,2370,2371,2373,2375,2377,2379,2381,2384],{"class":139,"line":931},[137,2372,745],{"class":157},[137,2374,353],{"class":147},[137,2376,356],{"class":157},[137,2378,1995],{"class":284},[137,2380,361],{"class":143},[137,2382,2383],{"class":157}," self.name);  ",[137,2385,541],{"class":308},[137,2387,2388,2391],{"class":139,"line":1568},[137,2389,2390],{"class":157},"            };                                            ",[137,2392,2393],{"class":308},"\u002F\u002F this here refers to the Window.\n",[137,2395,2396],{"class":139,"line":1573},[137,2397,2398],{"class":308},"                                                          \u002F\u002F we can use the reference we declared self which is refering to the Vue Component.\n",[137,2400,2401,2403,2405,2407,2409,2411],{"class":139,"line":1578},[137,2402,2067],{"class":147},[137,2404,356],{"class":157},[137,2406,934],{"class":147},[137,2408,2074],{"class":157},[137,2410,2077],{"class":364},[137,2412,1502],{"class":157},[137,2414,2415],{"class":139,"line":1588},[137,2416,2084],{"class":157},[137,2418,2419],{"class":139,"line":1601},[137,2420,510],{"class":157},[27,2422,2423],{},"And the last, and by far most common solution these days is to use an arrow function.",[128,2425,2427],{"className":130,"code":2426,"language":132,"meta":133,"style":133},"data() {\n    return {\n            name: \"Aleks\",\n        }\n},\nmethods: {\n        foo() {\n        console.log(\"My name is: \" + this.name); \u002F\u002F Output: My name is Aleks.\n                                                 \u002F\u002F `this` on this line, refers to the Vue Component.\n\n            var bar = () => {\n                console.log(\"My name is: \" + this.name); \u002F\u002F Output: My name is Aleks.\n            };                                           \u002F\u002F this here also refers to the Vue Component.\n                                                         \u002F\u002F the value of `this` is taken from the outer function, in this case ‘foo’.\n            setTimeout(bar(), 100);\n        },\n}\n",[22,2428,2429,2435,2441,2449,2453,2457,2463,2469,2487,2491,2495,2509,2527,2534,2539,2553,2557],{"__ignoreMap":133},[137,2430,2431,2433],{"class":139,"line":140},[137,2432,1942],{"class":147},[137,2434,275],{"class":157},[137,2436,2437,2439],{"class":139,"line":173},[137,2438,176],{"class":143},[137,2440,256],{"class":157},[137,2442,2443,2445,2447],{"class":139,"line":188},[137,2444,1955],{"class":157},[137,2446,1958],{"class":284},[137,2448,1961],{"class":157},[137,2450,2451],{"class":139,"line":269},[137,2452,1966],{"class":157},[137,2454,2455],{"class":139,"line":278},[137,2456,1971],{"class":157},[137,2458,2459,2461],{"class":139,"line":291},[137,2460,1816],{"class":147},[137,2462,1819],{"class":157},[137,2464,2465,2467],{"class":139,"line":297},[137,2466,1982],{"class":147},[137,2468,275],{"class":157},[137,2470,2471,2473,2475,2477,2479,2481,2483,2485],{"class":139,"line":302},[137,2472,350],{"class":157},[137,2474,353],{"class":147},[137,2476,356],{"class":157},[137,2478,1995],{"class":284},[137,2480,361],{"class":143},[137,2482,365],{"class":364},[137,2484,2002],{"class":157},[137,2486,541],{"class":308},[137,2488,2489],{"class":139,"line":662},[137,2490,2009],{"class":308},[137,2492,2493],{"class":139,"line":667},[137,2494,516],{"emptyLinePlaceholder":515},[137,2496,2497,2499,2501,2503,2505,2507],{"class":139,"line":786},[137,2498,2023],{"class":143},[137,2500,1479],{"class":147},[137,2502,151],{"class":143},[137,2504,1484],{"class":157},[137,2506,222],{"class":143},[137,2508,256],{"class":157},[137,2510,2511,2513,2515,2517,2519,2521,2523,2525],{"class":139,"line":798},[137,2512,745],{"class":157},[137,2514,353],{"class":147},[137,2516,356],{"class":157},[137,2518,1995],{"class":284},[137,2520,361],{"class":143},[137,2522,365],{"class":364},[137,2524,2002],{"class":157},[137,2526,541],{"class":308},[137,2528,2529,2531],{"class":139,"line":803},[137,2530,2054],{"class":157},[137,2532,2533],{"class":308},"\u002F\u002F this here also refers to the Vue Component.\n",[137,2535,2536],{"class":139,"line":931},[137,2537,2538],{"class":308},"                                                         \u002F\u002F the value of `this` is taken from the outer function, in this case ‘foo’.\n",[137,2540,2541,2543,2545,2547,2549,2551],{"class":139,"line":1568},[137,2542,2067],{"class":147},[137,2544,356],{"class":157},[137,2546,934],{"class":147},[137,2548,2074],{"class":157},[137,2550,2077],{"class":364},[137,2552,1502],{"class":157},[137,2554,2555],{"class":139,"line":1573},[137,2556,2084],{"class":157},[137,2558,2559],{"class":139,"line":1578},[137,2560,510],{"class":157},[27,2562,2563],{},"I hope this make sense!",[104,2565,2567],{"id":2566},"conclusion","Conclusion",[2569,2570,2571,2576,2586,2601,2610],"ol",{},[1006,2572,2573,2574,102],{},"The value of ",[22,2575,24],{},[1006,2577,2578,2579,114,2581,2583,2584,121],{},"In Javascript, we have two different types of functions, ",[42,2580,113],{},[42,2582,117],{},". They operate in almost the same manner, with one exception where they treat ",[22,2585,24],{},[1006,2587,425,2588,2590,2591,164,2593,164,2595,114,2597,1017],{},[22,2589,24],{}," in regular JavaScript function is determined by 4 rules: ",[42,2592,688],{},[42,2594,2097],{},[42,2596,2184],{},[42,2598,2599,1362],{},[22,2600,1361],{},[1006,2602,2603,2604,2606,2607,2609],{},"Unlike the regular functions where the value of ",[22,2605,24],{}," is determined by the 4 rules, the arrow functions uses ",[22,2608,24],{}," from the outer function, or the global scope in which it is declared.",[1006,2611,2612,2613,2615],{},"The most challenging part when working with ",[22,2614,24],{}," in Vue.JS, is when we declare a function inside the current function. There are several ways to fix that.",[2617,2618,2619],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":133,"searchDepth":173,"depth":173,"links":2621},[2622,2628,2630,2633,2636,2637,2638,2639,2641,2642,2647],{"id":106,"depth":173,"text":107,"children":2623},[2624,2625,2626,2627],{"id":125,"depth":188,"text":126},{"id":194,"depth":188,"text":195},{"id":237,"depth":188,"text":238},{"id":312,"depth":188,"text":313},{"id":408,"depth":173,"text":2629},"this in Regular functions",{"id":431,"depth":173,"text":432,"children":2631},[2632],{"id":472,"depth":188,"text":473},{"id":544,"depth":173,"text":545,"children":2634},[2635],{"id":598,"depth":188,"text":599},{"id":692,"depth":173,"text":693},{"id":822,"depth":173,"text":823},{"id":955,"depth":173,"text":956},{"id":1357,"depth":173,"text":2640},"4. new Binding",{"id":1447,"depth":173,"text":1448},{"id":1793,"depth":173,"text":2643,"children":2644},"Understanding this keyword in Vue.JS",[2645],{"id":2169,"depth":188,"text":2646},"How can we fix this",{"id":2566,"depth":173,"text":2567},"If you are new to Vue.JS you might have realised that \"this\" keyword is used everywhere. Some people might think that \"this\" keyword is part of the Vue.JS framework","md","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1618488085\u002Fblog\u002Fthe_misterios_this_keyword_in_vueland",[8,2652,2653,2654,2655,2656],"Javascript","Frontend","development","web development","this keyword",{},"\u002F2021\u002F05\u002F12\u002Fthe-mysterious-this-keyword-in-vueland","12th May 2021",{"title":6,"description":2648},"2021\u002F05\u002F12\u002Fthe-mysterious-this-keyword-in-vueland","page","uv4fb0BsezAgWtceCVIsD45He23SmFxBU3MFlaIIRVk",{"id":2665,"title":2666,"articleTags":2667,"author":11,"blog":12,"body":2670,"description":3534,"extension":2649,"image":3535,"keywords":3536,"meta":3543,"navigation":515,"path":3544,"published":3545,"readTime":662,"seo":3546,"stem":3547,"type":2662,"__hash__":3548},"content\u002F2021\u002F07\u002F13\u002Ffirebase-authentication-token-verification-with-a-custom-backend-server.md","Firebase Auth token verification with a custom backend server",[2668,9,2669],"Firebase","Node.js",{"type":14,"value":2671,"toc":3525},[2672,2675,2689,2691,2695,2700,2703,2706,2710,2727,2731,2734,2833,2836,2840,2847,2851,2854,2859,2862,2943,2947,2954,3058,3061,3065,3068,3243,3256,3259,3467,3470,3490,3493,3500,3502,3522],[17,2673,2666],{"id":2674},"firebase-auth-token-verification-with-a-custom-backend-server",[27,2676,2677],{},[30,2678,2679,36,2681,40,2683],{},[33,2680],{"value":35},[33,2682],{"value":39},[42,2684,2685],{},[45,2686,2687],{"href":47},[33,2688],{"value":50},[52,2690],{":tags":54},[56,2692],{":audio-src":2693,":transcript-src":2694},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F07\u002F13\u002Ffirebase-authentication-token-verification-with-a-custom-backend-server\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F07\u002F13\u002Ffirebase-authentication-token-verification-with-a-custom-backend-server\u002Fsummary.json",[27,2696,2697],{},[63,2698],{"alt":65,"src":2699},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1626001455\u002Fblog\u002Ffirebase-authentication-token-verification-with-a-custom-backend-server",[27,2701,2702],{},"Firebase as a platform that offers a wide range of services to developers to build, improve, and grow their apps with little or almost no effort. This includes services like authentication, databases, analytics, file storage, push messaging and more. When it comes to user authentication, Firebase provides an Authentication service that allows for codes to be written in order for users to be logged into an app right from the client side, and limit user access to resources in other Firebase products. This is fairly simple to use without the need to implement any backend solution. Firebase also provides an Admin SDK that allows developers to build a custom backend if required.",[27,2704,2705],{},"In this article we will have a look at few examples on how we can use token ID provided by Firebase client side to verify our user on a custom backend using Node.js. Before we continue, do note that this is not an introduction to Firebase. A basic understanding of Firebase and JavaScript is required before reading on about the examples that I'm about to explain.",[104,2707,2709],{"id":2708},"connect-your-app-to-firebase","Connect your app to Firebase",[27,2711,2712,2713,2721,2722,1017],{},"Before we start working with Firebase we need to create a Firebase project in the ",[45,2714,2720],{"href":2715,"target":2716,"rel":2717},"https:\u002F\u002Fconsole.firebase.google.com\u002F?authuser=0","_blank",[2718,2719],"noopener","noreferrer","Firebase console",". We will then add and initialise the Firebase SDK to our web app. For a detailed explanation on how to set up Firebase to your JavaScript project, follow the instructions on this ",[45,2723,2726],{"href":2724,"target":2716,"rel":2725},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fweb\u002Fsetup",[2718,2719],"link",[104,2728,2730],{"id":2729},"user-authenticate-from-the-client-side","User authenticate from the client side",[27,2732,2733],{},"The example below shows user authentication using email and password on the Firebase client side SDK. As we can see it is an easy and straight forward solution without the need of backend server.",[128,2735,2737],{"className":130,"code":2736,"language":132,"meta":133,"style":133},"firebase\n    .auth()\n    .signInWithEmailAndPassword(email, password)\n    .then((userCredential) => {\n        console.log(userCredential.user); \u002F\u002F Signed in\n    })\n    .catch((error) => {\n        console.log(error.code, error.message);\n    });\n",[22,2738,2739,2744,2755,2765,2784,2796,2801,2819,2828],{"__ignoreMap":133},[137,2740,2741],{"class":139,"line":140},[137,2742,2743],{"class":157},"firebase\n",[137,2745,2746,2749,2752],{"class":139,"line":173},[137,2747,2748],{"class":157},"    .",[137,2750,2751],{"class":147},"auth",[137,2753,2754],{"class":157},"()\n",[137,2756,2757,2759,2762],{"class":139,"line":188},[137,2758,2748],{"class":157},[137,2760,2761],{"class":147},"signInWithEmailAndPassword",[137,2763,2764],{"class":157},"(email, password)\n",[137,2766,2767,2769,2772,2775,2778,2780,2782],{"class":139,"line":269},[137,2768,2748],{"class":157},[137,2770,2771],{"class":147},"then",[137,2773,2774],{"class":157},"((",[137,2776,2777],{"class":161},"userCredential",[137,2779,219],{"class":157},[137,2781,222],{"class":143},[137,2783,256],{"class":157},[137,2785,2786,2788,2790,2793],{"class":139,"line":278},[137,2787,350],{"class":157},[137,2789,353],{"class":147},[137,2791,2792],{"class":157},"(userCredential.user); ",[137,2794,2795],{"class":308},"\u002F\u002F Signed in\n",[137,2797,2798],{"class":139,"line":291},[137,2799,2800],{"class":157},"    })\n",[137,2802,2803,2805,2808,2810,2813,2815,2817],{"class":139,"line":297},[137,2804,2748],{"class":157},[137,2806,2807],{"class":147},"catch",[137,2809,2774],{"class":157},[137,2811,2812],{"class":161},"error",[137,2814,219],{"class":157},[137,2816,222],{"class":143},[137,2818,256],{"class":157},[137,2820,2821,2823,2825],{"class":139,"line":302},[137,2822,350],{"class":157},[137,2824,353],{"class":147},[137,2826,2827],{"class":157},"(error.code, error.message);\n",[137,2829,2830],{"class":139,"line":662},[137,2831,2832],{"class":157},"    });\n",[27,2834,2835],{},"What happens if we would like to add on verifications for our signed in user on the custom backend server?",[104,2837,2839],{"id":2838},"verify-user-on-the-backend-server","Verify user on the backend server",[27,2841,2842,2843,1017],{},"To be able to interact with Firebase from a backend server we need to use the Firebase Admin SDK. For a detailed explanation on how to set up Firebase on the backend server, follow the instructions on this ",[45,2844,2726],{"href":2845,"target":2716,"rel":2846},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fadmin\u002Fsetup",[2718,2719],[123,2848,2850],{"id":2849},"verify-id-tokens","Verify ID Tokens",[27,2852,2853],{},"If the user is signed in from the client app, how would we identify that user on the server. To do so, in a secure manner, we will first send the user ID token to our server from the client via HTTPS. Then, on the backend server, verify the integrity and authenticity of the ID token and retrieve the user ID (uid) from it.",[2855,2856,2858],"h4",{"id":2857},"retrieve-id-token-on-client-side","Retrieve ID token on client side",[27,2860,2861],{},"When we sign in Firebase creates ID token that uniquely identifies the user. This ID token can be re-used to identify the user on our custom backend server. See the example below on how to get the ID token from the signed-in user:",[128,2863,2865],{"className":130,"code":2864,"language":132,"meta":133,"style":133},"firebase\n    .auth()\n    .currentUser.getIdToken()\n    .then((idToken) => {\n        \u002F\u002F Send token to your backend using HTTPS\n    })\n    .catch((error) => {\n        console.log(error.code, error.message);\n    });\n",[22,2866,2867,2871,2879,2889,2906,2911,2915,2931,2939],{"__ignoreMap":133},[137,2868,2869],{"class":139,"line":140},[137,2870,2743],{"class":157},[137,2872,2873,2875,2877],{"class":139,"line":173},[137,2874,2748],{"class":157},[137,2876,2751],{"class":147},[137,2878,2754],{"class":157},[137,2880,2881,2884,2887],{"class":139,"line":188},[137,2882,2883],{"class":157},"    .currentUser.",[137,2885,2886],{"class":147},"getIdToken",[137,2888,2754],{"class":157},[137,2890,2891,2893,2895,2897,2900,2902,2904],{"class":139,"line":269},[137,2892,2748],{"class":157},[137,2894,2771],{"class":147},[137,2896,2774],{"class":157},[137,2898,2899],{"class":161},"idToken",[137,2901,219],{"class":157},[137,2903,222],{"class":143},[137,2905,256],{"class":157},[137,2907,2908],{"class":139,"line":278},[137,2909,2910],{"class":308},"        \u002F\u002F Send token to your backend using HTTPS\n",[137,2912,2913],{"class":139,"line":291},[137,2914,2800],{"class":157},[137,2916,2917,2919,2921,2923,2925,2927,2929],{"class":139,"line":297},[137,2918,2748],{"class":157},[137,2920,2807],{"class":147},[137,2922,2774],{"class":157},[137,2924,2812],{"class":161},[137,2926,219],{"class":157},[137,2928,222],{"class":143},[137,2930,256],{"class":157},[137,2932,2933,2935,2937],{"class":139,"line":302},[137,2934,350],{"class":157},[137,2936,353],{"class":147},[137,2938,2827],{"class":157},[137,2940,2941],{"class":139,"line":662},[137,2942,2832],{"class":157},[2855,2944,2946],{"id":2945},"verify-id-token-on-the-backend-server","Verify ID token on the backend server",[27,2948,2949,2950,2953],{},"Once the ID token has been passed from the client app, we use the build-in method ",[22,2951,2952],{},"verifyIdToken()"," from the Firebase Admin SDK to verify and decode the ID token on the server. If the provided ID token has the correct format, is not expired, and is properly signed-in, then we can grab the uid of the user.",[128,2955,2957],{"className":130,"code":2956,"language":132,"meta":133,"style":133},"\u002F\u002F idToken comes from the client app\nadmin\n    .auth()\n    .verifyIdToken(idToken)\n    .then((decodedToken) => {\n        const uid = decodedToken.uid;\n        console.log(uid);\n    })\n    .catch((error) => {\n        console.log(error.code, error.message);\n    });\n",[22,2958,2959,2964,2969,2977,2987,3004,3017,3026,3030,3046,3054],{"__ignoreMap":133},[137,2960,2961],{"class":139,"line":140},[137,2962,2963],{"class":308},"\u002F\u002F idToken comes from the client app\n",[137,2965,2966],{"class":139,"line":173},[137,2967,2968],{"class":157},"admin\n",[137,2970,2971,2973,2975],{"class":139,"line":188},[137,2972,2748],{"class":157},[137,2974,2751],{"class":147},[137,2976,2754],{"class":157},[137,2978,2979,2981,2984],{"class":139,"line":269},[137,2980,2748],{"class":157},[137,2982,2983],{"class":147},"verifyIdToken",[137,2985,2986],{"class":157},"(idToken)\n",[137,2988,2989,2991,2993,2995,2998,3000,3002],{"class":139,"line":278},[137,2990,2748],{"class":157},[137,2992,2771],{"class":147},[137,2994,2774],{"class":157},[137,2996,2997],{"class":161},"decodedToken",[137,2999,219],{"class":157},[137,3001,222],{"class":143},[137,3003,256],{"class":157},[137,3005,3006,3009,3012,3014],{"class":139,"line":291},[137,3007,3008],{"class":143},"        const",[137,3010,3011],{"class":364}," uid",[137,3013,151],{"class":143},[137,3015,3016],{"class":157}," decodedToken.uid;\n",[137,3018,3019,3021,3023],{"class":139,"line":297},[137,3020,350],{"class":157},[137,3022,353],{"class":147},[137,3024,3025],{"class":157},"(uid);\n",[137,3027,3028],{"class":139,"line":302},[137,3029,2800],{"class":157},[137,3031,3032,3034,3036,3038,3040,3042,3044],{"class":139,"line":662},[137,3033,2748],{"class":157},[137,3035,2807],{"class":147},[137,3037,2774],{"class":157},[137,3039,2812],{"class":161},[137,3041,219],{"class":157},[137,3043,222],{"class":143},[137,3045,256],{"class":157},[137,3047,3048,3050,3052],{"class":139,"line":667},[137,3049,350],{"class":157},[137,3051,353],{"class":147},[137,3053,2827],{"class":157},[137,3055,3056],{"class":139,"line":786},[137,3057,2832],{"class":157},[27,3059,3060],{},"While this is a useful way to verify users on the backend server, there is one limitation faced whilst building my app. The ID tokens have a validity period of only one hour. In other words, once the ID token has been created, it lives for one hour even after the user has been sign out. Currently, there is no Firebase API to check if the user has been signed-in\u002Fout using the Admin SDK. As a work around to overcoming this limitation, continue reading the next section below.",[104,3062,3064],{"id":3063},"solution","Solution",[27,3066,3067],{},"Before we make a request to the backend server from our client app, we need to store the user ID token somewhere in the database. In our case we are going to use Firestore. We next compared the stored ID token in our database to the ID token of the user that we requested. Here are some examples below on how we can store our ID token in Firestore.",[128,3069,3071],{"className":130,"code":3070,"language":132,"meta":133,"style":133},"const tokenId = firebase.auth().currentUser.getIdToken(true); \u002F\u002F With \"true\", we force refresh of new token\nconst userUid = firebase.auth().currentUser.uid;\n\n\u002F\u002F Add a new document in collection \"tokens\". We are using uid to name the document\nfirebase\n    .firestore()\n    .collection(\"tokens\")\n    .doc(userUid)\n    .update({\n        token: tokenId,\n    })\n    .then(() => {\n        console.log(\"Token successfully written in the database!\");\n    })\n    .catch((error) => {\n        console.log(error.code, error.message);\n    });\n",[22,3072,3073,3103,3119,3123,3128,3132,3141,3156,3166,3176,3181,3185,3198,3211,3215,3231,3239],{"__ignoreMap":133},[137,3074,3075,3078,3081,3083,3086,3088,3091,3093,3095,3098,3100],{"class":139,"line":140},[137,3076,3077],{"class":143},"const",[137,3079,3080],{"class":364}," tokenId",[137,3082,151],{"class":143},[137,3084,3085],{"class":157}," firebase.",[137,3087,2751],{"class":147},[137,3089,3090],{"class":157},"().currentUser.",[137,3092,2886],{"class":147},[137,3094,356],{"class":157},[137,3096,3097],{"class":364},"true",[137,3099,1839],{"class":157},[137,3101,3102],{"class":308},"\u002F\u002F With \"true\", we force refresh of new token\n",[137,3104,3105,3107,3110,3112,3114,3116],{"class":139,"line":173},[137,3106,3077],{"class":143},[137,3108,3109],{"class":364}," userUid",[137,3111,151],{"class":143},[137,3113,3085],{"class":157},[137,3115,2751],{"class":147},[137,3117,3118],{"class":157},"().currentUser.uid;\n",[137,3120,3121],{"class":139,"line":188},[137,3122,516],{"emptyLinePlaceholder":515},[137,3124,3125],{"class":139,"line":269},[137,3126,3127],{"class":308},"\u002F\u002F Add a new document in collection \"tokens\". We are using uid to name the document\n",[137,3129,3130],{"class":139,"line":278},[137,3131,2743],{"class":157},[137,3133,3134,3136,3139],{"class":139,"line":291},[137,3135,2748],{"class":157},[137,3137,3138],{"class":147},"firestore",[137,3140,2754],{"class":157},[137,3142,3143,3145,3148,3150,3153],{"class":139,"line":297},[137,3144,2748],{"class":157},[137,3146,3147],{"class":147},"collection",[137,3149,356],{"class":157},[137,3151,3152],{"class":284},"\"tokens\"",[137,3154,3155],{"class":157},")\n",[137,3157,3158,3160,3163],{"class":139,"line":302},[137,3159,2748],{"class":157},[137,3161,3162],{"class":147},"doc",[137,3164,3165],{"class":157},"(userUid)\n",[137,3167,3168,3170,3173],{"class":139,"line":662},[137,3169,2748],{"class":157},[137,3171,3172],{"class":147},"update",[137,3174,3175],{"class":157},"({\n",[137,3177,3178],{"class":139,"line":667},[137,3179,3180],{"class":157},"        token: tokenId,\n",[137,3182,3183],{"class":139,"line":786},[137,3184,2800],{"class":157},[137,3186,3187,3189,3191,3194,3196],{"class":139,"line":798},[137,3188,2748],{"class":157},[137,3190,2771],{"class":147},[137,3192,3193],{"class":157},"(() ",[137,3195,222],{"class":143},[137,3197,256],{"class":157},[137,3199,3200,3202,3204,3206,3209],{"class":139,"line":803},[137,3201,350],{"class":157},[137,3203,353],{"class":147},[137,3205,356],{"class":157},[137,3207,3208],{"class":284},"\"Token successfully written in the database!\"",[137,3210,1502],{"class":157},[137,3212,3213],{"class":139,"line":931},[137,3214,2800],{"class":157},[137,3216,3217,3219,3221,3223,3225,3227,3229],{"class":139,"line":1568},[137,3218,2748],{"class":157},[137,3220,2807],{"class":147},[137,3222,2774],{"class":157},[137,3224,2812],{"class":161},[137,3226,219],{"class":157},[137,3228,222],{"class":143},[137,3230,256],{"class":157},[137,3232,3233,3235,3237],{"class":139,"line":1573},[137,3234,350],{"class":157},[137,3236,353],{"class":147},[137,3238,2827],{"class":157},[137,3240,3241],{"class":139,"line":1578},[137,3242,2832],{"class":157},[3244,3245,3246],"blockquote",{},[27,3247,3248,3251,3252,1017],{},[42,3249,3250],{},"Note:"," We first need to create the collection \"tokens\" with the document named with the user \"uid\" before hand. Ideally, that would be when a user has been created. We can trigger Cloud Functions on user creation and deletion. Please refer to the following link for more details on how we can trigger Cloud Functions when user has been created ",[45,3253,2726],{"href":3254,"target":2716,"rel":3255},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Ffunctions\u002Fauth-events",[2718,2719],[27,3257,3258],{},"Now we need to add security rules to our Firestore where only the signed-in user can update its token based on its own uid. The Firestore rules are located in the Firebase console, under the Rules tab in the Firestore Database tab. Here is an example on how to add security rules.",[128,3260,3262],{"className":130,"code":3261,"language":132,"meta":133,"style":133},"rules_version = '2';\nservice cloud.firestore {\n  match \u002Fdatabases\u002F{database}\u002Fdocuments {\n    match \u002Ftokens\u002F{id}\u002F{u=**} {\n      allow read, write: if (isSignedIn() && isUser(id));\n    }\n    match \u002F{document=**} {\n      allow read, write: if false;\n    }\n    function isSignedIn() {\n      return request.auth != null;\n    }\n\n    function isUser(uid) {\n      return uid == request.auth.uid;\n    }\n  }\n}\n",[22,3263,3264,3277,3282,3302,3328,3358,3362,3375,3390,3394,3404,3420,3424,3428,3441,3454,3458,3463],{"__ignoreMap":133},[137,3265,3266,3269,3271,3274],{"class":139,"line":140},[137,3267,3268],{"class":157},"rules_version ",[137,3270,253],{"class":143},[137,3272,3273],{"class":284}," '2'",[137,3275,3276],{"class":157},";\n",[137,3278,3279],{"class":139,"line":173},[137,3280,3281],{"class":157},"service cloud.firestore {\n",[137,3283,3284,3287,3289,3292,3294,3297,3299],{"class":139,"line":188},[137,3285,3286],{"class":157},"  match ",[137,3288,47],{"class":143},[137,3290,3291],{"class":157},"databases",[137,3293,47],{"class":143},[137,3295,3296],{"class":157},"{database}",[137,3298,47],{"class":143},[137,3300,3301],{"class":157},"documents {\n",[137,3303,3304,3307,3309,3312,3314,3317,3319,3322,3325],{"class":139,"line":269},[137,3305,3306],{"class":157},"    match ",[137,3308,47],{"class":143},[137,3310,3311],{"class":157},"tokens",[137,3313,47],{"class":143},[137,3315,3316],{"class":157},"{id}",[137,3318,47],{"class":143},[137,3320,3321],{"class":157},"{u",[137,3323,3324],{"class":143},"=**",[137,3326,3327],{"class":157},"} {\n",[137,3329,3330,3333,3336,3338,3341,3343,3346,3349,3352,3355],{"class":139,"line":278},[137,3331,3332],{"class":157},"      allow read, ",[137,3334,3335],{"class":147},"write",[137,3337,726],{"class":157},[137,3339,3340],{"class":143},"if",[137,3342,158],{"class":157},[137,3344,3345],{"class":147},"isSignedIn",[137,3347,3348],{"class":157},"() ",[137,3350,3351],{"class":143},"&&",[137,3353,3354],{"class":147}," isUser",[137,3356,3357],{"class":157},"(id));\n",[137,3359,3360],{"class":139,"line":291},[137,3361,294],{"class":157},[137,3363,3364,3366,3368,3371,3373],{"class":139,"line":297},[137,3365,3306],{"class":157},[137,3367,47],{"class":143},[137,3369,3370],{"class":157},"{document",[137,3372,3324],{"class":143},[137,3374,3327],{"class":157},[137,3376,3377,3379,3381,3383,3385,3388],{"class":139,"line":302},[137,3378,3332],{"class":157},[137,3380,3335],{"class":147},[137,3382,726],{"class":157},[137,3384,3340],{"class":143},[137,3386,3387],{"class":364}," false",[137,3389,3276],{"class":157},[137,3391,3392],{"class":139,"line":662},[137,3393,294],{"class":157},[137,3395,3396,3399,3402],{"class":139,"line":667},[137,3397,3398],{"class":143},"    function",[137,3400,3401],{"class":147}," isSignedIn",[137,3403,275],{"class":157},[137,3405,3406,3409,3412,3415,3418],{"class":139,"line":786},[137,3407,3408],{"class":143},"      return",[137,3410,3411],{"class":157}," request.auth ",[137,3413,3414],{"class":143},"!=",[137,3416,3417],{"class":364}," null",[137,3419,3276],{"class":157},[137,3421,3422],{"class":139,"line":798},[137,3423,294],{"class":157},[137,3425,3426],{"class":139,"line":803},[137,3427,516],{"emptyLinePlaceholder":515},[137,3429,3430,3432,3434,3436,3439],{"class":139,"line":931},[137,3431,3398],{"class":143},[137,3433,3354],{"class":147},[137,3435,356],{"class":157},[137,3437,3438],{"class":161},"uid",[137,3440,170],{"class":157},[137,3442,3443,3445,3448,3451],{"class":139,"line":1568},[137,3444,3408],{"class":143},[137,3446,3447],{"class":157}," uid ",[137,3449,3450],{"class":143},"==",[137,3452,3453],{"class":157}," request.auth.uid;\n",[137,3455,3456],{"class":139,"line":1573},[137,3457,294],{"class":157},[137,3459,3460],{"class":139,"line":1578},[137,3461,3462],{"class":157},"  }\n",[137,3464,3465],{"class":139,"line":1588},[137,3466,510],{"class":157},[27,3468,3469],{},"The explanation of the code above can be found below:",[1003,3471,3472,3475,3478,3484],{},[1006,3473,3474],{},"Documents under the collection \"tokens\" can be read and written only if their document id is the same as the sent request's uid.",[1006,3476,3477],{},"The rest of the collection are only accessible from the backend server using the Firebase Admin SDK.",[1006,3479,3480,3483],{},[22,3481,3482],{},"isSignedIn()"," function checks if request is authorised.",[1006,3485,3486,3489],{},[22,3487,3488],{},"isUser(id)"," function checks if id matches the authorised request's uid.",[27,3491,3492],{},"Lastly on the backend server we compare both ID tokens, one that we've received from the client app and the one we have stored in the Firestore. If the tokens are the same, then the user is granted access to resources in other Firebase products.",[3244,3494,3495],{},[27,3496,3497,3499],{},[42,3498,3250],{}," To avoid potential security attacks, make sure you remove the user ID token in the Firestore document when user has signed-out.",[104,3501,2567],{"id":2566},[2569,3503,3504,3507,3510,3513,3519],{},[1006,3505,3506],{},"Firebase as a platform offers a wide range of services to developers to build, improve, and grow their apps with little or almost no effort.",[1006,3508,3509],{},"Users can be authenticated from the client app using Firebase Client SDK without a custom backend server.",[1006,3511,3512],{},"To be able to interact with Firebase from a backend server, use the Firebase Admin SDK.",[1006,3514,3515,3516,3518],{},"For verifying ID Tokens on the server, use the build-in method ",[22,3517,2952],{}," from the Firebase Admin SDK to verify and decode the ID token on the server. However, note the limitation of ID tokens having a validity period of only one hour; and the ID token remaining active even after the user has signed-out.",[1006,3520,3521],{},"A solution to verify only signed-in users on the server is by storing the user ID token in the Firestore and comparing that with the ID token that has been sent from the client app.",[2617,3523,3524],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":133,"searchDepth":173,"depth":173,"links":3526},[3527,3528,3529,3532,3533],{"id":2708,"depth":173,"text":2709},{"id":2729,"depth":173,"text":2730},{"id":2838,"depth":173,"text":2839,"children":3530},[3531],{"id":2849,"depth":188,"text":2850},{"id":3063,"depth":173,"text":3064},{"id":2566,"depth":173,"text":2567},"Firebase as a platform that offers a wide range of services to developers to build, improve, and grow their apps with little or almost no effort. This includes services like authentication, databases,  analytics, file storage, push messaging and more. When it comes to user authentication, Firebase provides an Authentication service that allows for codes to be written in order for users to be logged into an app right from the client side, and limit user access to resources in other Firebase products. This is fairly simple to use without the need to implement any backend solution. Firebase also provides an Admin SDK that allows developers to build a custom backend if required.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1626001455\u002Fblog\u002Ffirebase-authentication-token-verification-with-a-custom-backend-server",[2668,3537,3538,2652,3539,3540,3541,3542,2654,2655],"Authentication","Firestore","Node JS","Token","Token Verification","Frontend, Backend",{},"\u002F2021\u002F07\u002F13\u002Ffirebase-authentication-token-verification-with-a-custom-backend-server","13th Jul 2021",{"title":2666,"description":3534},"2021\u002F07\u002F13\u002Ffirebase-authentication-token-verification-with-a-custom-backend-server","Z02zSCvxIk2CGO_swnMMJMrAJrqWV058uwHP_Qfa1ME",{"id":3550,"title":3551,"articleTags":3552,"author":11,"blog":12,"body":3553,"description":5291,"extension":2649,"image":5292,"keywords":5293,"meta":5301,"navigation":515,"path":5302,"published":5303,"readTime":662,"seo":5304,"stem":5305,"type":2662,"__hash__":5306},"content\u002F2021\u002F07\u002F24\u002Fcreate-a-custom-html-element-using-web-components-and-vuejs.md","Create a custom HTML element using Web Components and Vue.js",[9,10,8],{"type":14,"value":3554,"toc":5284},[3555,3558,3572,3574,3578,3583,3586,3590,3604,3611,3616,3619,3623,3626,3926,3941,3959,3981,3988,3995,4019,4022,4088,4091,4096,4104,4108,4111,4118,4284,4308,4317,4368,4384,4735,4747,4783,4795,5017,5023,5042,5048,5101,5129,5133,5146,5153,5181,5184,5192,5195,5248,5250,5274,5281],[17,3556,3551],{"id":3557},"create-a-custom-html-element-using-web-components-and-vuejs",[27,3559,3560],{},[30,3561,3562,36,3564,40,3566],{},[33,3563],{"value":35},[33,3565],{"value":39},[42,3567,3568],{},[45,3569,3570],{"href":47},[33,3571],{"value":50},[52,3573],{":tags":54},[56,3575],{":audio-src":3576,":transcript-src":3577},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F07\u002F24\u002Fcreate-a-custom-html-element-using-web-components-and-vuejs\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F07\u002F24\u002Fcreate-a-custom-html-element-using-web-components-and-vuejs\u002Fsummary.json",[27,3579,3580],{},[63,3581],{"alt":65,"src":3582},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1627371334\u002Fblog\u002Fcreate-a-custom-HTML-element-using-Web-Components-and-Vuejs",[27,3584,3585],{},"In this day and age writing reusable components that can be used multiple times in your application or shared between other web platforms can actually save you a lot of time and effort. Web Components offer this benefit and makes it easier to build custom complex components. At the same time, this improves its reusability.",[104,3587,3589],{"id":3588},"what-are-web-components","What are Web Components?",[27,3591,3592,3593,3596,3597,3596,3600,3603],{},"Web components are a set of web platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps. Think native HTML elements ",[22,3594,3595],{},"\u003Cdiv>"," ",[22,3598,3599],{},"\u003Csection>",[22,3601,3602],{},"\u003Cbutton>",", but something that we can create ourselves with encapsulated functionality that can be reused across modern web browsers or any JavaScript library or framework that works with HTML.",[27,3605,3606,3607,3610],{},"For example, let's imagine that we've created a custom reusable HTML element ",[22,3608,3609],{},"\u003Cuser-register>"," and subsequently used this in every web application that we need to register new users. See picture below.",[27,3612,3613],{},[63,3614],{"alt":65,"src":3615},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1627387526\u002Fblog\u002Fregister-form",[27,3617,3618],{},"We will be creating this component later in the article using Web Components and Vue.js. However, before this takes place, we will need to build a simple web component.",[104,3620,3622],{"id":3621},"create-a-web-component","Create a Web Component",[27,3624,3625],{},"To begin with let's have a look at an example of a custom HTML element created with Web Components.",[128,3627,3629],{"className":130,"code":3628,"language":132,"meta":133,"style":133},"class CustomElement extends HTMLElement {\n    constructor() {\n        \u002F\u002F Always call super first in constructor\n        super();\n\n        \u002F\u002F Create a shadow root\n        this.attachShadow({ mode: \"open\" }); \u002F\u002F sets and returns 'this.shadowRoot'\n\n        \u002F\u002F Create a paragraph with an id of \"paragraph\"\n        const paragraph = document.createElement(\"p\");\n        paragraph.id = \"paragraph\";\n\n        \u002F\u002F Create some CSS to apply to the shadow dom\n        const style = document.createElement(\"style\");\n        style.textContent = `\n          p {\n           font-size: 42px;\n          }`;\n\n        \u002F\u002F Attach the created elements to the shadow DOM\n        this.shadowRoot.append(style, paragraph);\n    }\n\n    connectedCallback() {\n        \u002F\u002F This runs each time the element is added to the DOM\n        const shadow = this.shadowRoot;\n        const paragraph = shadow.querySelector(\"#paragraph\");\n        paragraph.innerHTML = \"This is a paragraph!\";\n    }\n}\n\ncustomElements.define(\"custom-element\", CustomElement);\n",[22,3630,3631,3647,3654,3659,3666,3670,3675,3697,3701,3706,3728,3740,3744,3749,3769,3779,3784,3789,3796,3800,3806,3820,3825,3830,3838,3844,3859,3881,3894,3899,3904,3909],{"__ignoreMap":133},[137,3632,3633,3636,3639,3642,3645],{"class":139,"line":140},[137,3634,3635],{"class":143},"class",[137,3637,3638],{"class":147}," CustomElement",[137,3640,3641],{"class":143}," extends",[137,3643,3644],{"class":147}," HTMLElement",[137,3646,256],{"class":157},[137,3648,3649,3652],{"class":139,"line":173},[137,3650,3651],{"class":143},"    constructor",[137,3653,275],{"class":157},[137,3655,3656],{"class":139,"line":188},[137,3657,3658],{"class":308},"        \u002F\u002F Always call super first in constructor\n",[137,3660,3661,3664],{"class":139,"line":269},[137,3662,3663],{"class":364},"        super",[137,3665,924],{"class":157},[137,3667,3668],{"class":139,"line":278},[137,3669,516],{"emptyLinePlaceholder":515},[137,3671,3672],{"class":139,"line":291},[137,3673,3674],{"class":308},"        \u002F\u002F Create a shadow root\n",[137,3676,3677,3680,3682,3685,3688,3691,3694],{"class":139,"line":297},[137,3678,3679],{"class":364},"        this",[137,3681,1017],{"class":157},[137,3683,3684],{"class":147},"attachShadow",[137,3686,3687],{"class":157},"({ mode: ",[137,3689,3690],{"class":284},"\"open\"",[137,3692,3693],{"class":157}," }); ",[137,3695,3696],{"class":308},"\u002F\u002F sets and returns 'this.shadowRoot'\n",[137,3698,3699],{"class":139,"line":302},[137,3700,516],{"emptyLinePlaceholder":515},[137,3702,3703],{"class":139,"line":662},[137,3704,3705],{"class":308},"        \u002F\u002F Create a paragraph with an id of \"paragraph\"\n",[137,3707,3708,3710,3713,3715,3718,3721,3723,3726],{"class":139,"line":667},[137,3709,3008],{"class":143},[137,3711,3712],{"class":364}," paragraph",[137,3714,151],{"class":143},[137,3716,3717],{"class":157}," document.",[137,3719,3720],{"class":147},"createElement",[137,3722,356],{"class":157},[137,3724,3725],{"class":284},"\"p\"",[137,3727,1502],{"class":157},[137,3729,3730,3733,3735,3738],{"class":139,"line":786},[137,3731,3732],{"class":157},"        paragraph.id ",[137,3734,253],{"class":143},[137,3736,3737],{"class":284}," \"paragraph\"",[137,3739,3276],{"class":157},[137,3741,3742],{"class":139,"line":798},[137,3743,516],{"emptyLinePlaceholder":515},[137,3745,3746],{"class":139,"line":803},[137,3747,3748],{"class":308},"        \u002F\u002F Create some CSS to apply to the shadow dom\n",[137,3750,3751,3753,3756,3758,3760,3762,3764,3767],{"class":139,"line":931},[137,3752,3008],{"class":143},[137,3754,3755],{"class":364}," style",[137,3757,151],{"class":143},[137,3759,3717],{"class":157},[137,3761,3720],{"class":147},[137,3763,356],{"class":157},[137,3765,3766],{"class":284},"\"style\"",[137,3768,1502],{"class":157},[137,3770,3771,3774,3776],{"class":139,"line":1568},[137,3772,3773],{"class":157},"        style.textContent ",[137,3775,253],{"class":143},[137,3777,3778],{"class":284}," `\n",[137,3780,3781],{"class":139,"line":1573},[137,3782,3783],{"class":284},"          p {\n",[137,3785,3786],{"class":139,"line":1578},[137,3787,3788],{"class":284},"           font-size: 42px;\n",[137,3790,3791,3794],{"class":139,"line":1588},[137,3792,3793],{"class":284},"          }`",[137,3795,3276],{"class":157},[137,3797,3798],{"class":139,"line":1601},[137,3799,516],{"emptyLinePlaceholder":515},[137,3801,3803],{"class":139,"line":3802},20,[137,3804,3805],{"class":308},"        \u002F\u002F Attach the created elements to the shadow DOM\n",[137,3807,3809,3811,3814,3817],{"class":139,"line":3808},21,[137,3810,3679],{"class":364},[137,3812,3813],{"class":157},".shadowRoot.",[137,3815,3816],{"class":147},"append",[137,3818,3819],{"class":157},"(style, paragraph);\n",[137,3821,3823],{"class":139,"line":3822},22,[137,3824,294],{"class":157},[137,3826,3828],{"class":139,"line":3827},23,[137,3829,516],{"emptyLinePlaceholder":515},[137,3831,3833,3836],{"class":139,"line":3832},24,[137,3834,3835],{"class":147},"    connectedCallback",[137,3837,275],{"class":157},[137,3839,3841],{"class":139,"line":3840},25,[137,3842,3843],{"class":308},"        \u002F\u002F This runs each time the element is added to the DOM\n",[137,3845,3847,3849,3852,3854,3856],{"class":139,"line":3846},26,[137,3848,3008],{"class":143},[137,3850,3851],{"class":364}," shadow",[137,3853,151],{"class":143},[137,3855,365],{"class":364},[137,3857,3858],{"class":157},".shadowRoot;\n",[137,3860,3862,3864,3866,3868,3871,3874,3876,3879],{"class":139,"line":3861},27,[137,3863,3008],{"class":143},[137,3865,3712],{"class":364},[137,3867,151],{"class":143},[137,3869,3870],{"class":157}," shadow.",[137,3872,3873],{"class":147},"querySelector",[137,3875,356],{"class":157},[137,3877,3878],{"class":284},"\"#paragraph\"",[137,3880,1502],{"class":157},[137,3882,3884,3887,3889,3892],{"class":139,"line":3883},28,[137,3885,3886],{"class":157},"        paragraph.innerHTML ",[137,3888,253],{"class":143},[137,3890,3891],{"class":284}," \"This is a paragraph!\"",[137,3893,3276],{"class":157},[137,3895,3897],{"class":139,"line":3896},29,[137,3898,294],{"class":157},[137,3900,3902],{"class":139,"line":3901},30,[137,3903,510],{"class":157},[137,3905,3907],{"class":139,"line":3906},31,[137,3908,516],{"emptyLinePlaceholder":515},[137,3910,3912,3915,3918,3920,3923],{"class":139,"line":3911},32,[137,3913,3914],{"class":157},"customElements.",[137,3916,3917],{"class":147},"define",[137,3919,356],{"class":157},[137,3921,3922],{"class":284},"\"custom-element\"",[137,3924,3925],{"class":157},", CustomElement);\n",[27,3927,3928,3929,3932,3933,3936,3937,3940],{},"The JavaScript file defines a class called ",[22,3930,3931],{},"CustomElement",", which extends the generic HTMLElement class. We always start by calling ",[22,3934,3935],{},"super()"," in the class ",[22,3938,3939],{},"constructor()"," method so that the correct prototype chain is established. Within the constructor, we will define all functionalities of the element when it is initialised.",[27,3942,3943,3944,3947,3948,3951,3952,3955,3956,1017],{},"An important aspect of web components is encapsulation. The Shadow DOM API is a key part of this, as it provides a way to attach a hidden separated DOM to an element. We can attach a shadow root to any element using the ",[22,3945,3946],{},"attachShadow()"," method with an options object that contains one option ",[22,3949,3950],{},"mode"," with a value of ",[22,3953,3954],{},"open"," or ",[22,3957,3958],{},"closed",[27,3960,3961,3962,3965,3966,3969,3970,3973,3974,3977,3978,1017],{},"After attaching the shadow root to the custom element, we next use some DOM manipulation to create the element's internal shadow DOM structure. In our case, we will create ",[22,3963,3964],{},"\u003Cp>"," HTML element with an id of \"",[42,3967,3968],{},"paragraph","\" and a ",[22,3971,3972],{},"\u003Cstyle>"," element with some basic style of ",[22,3975,3976],{},"font-size: 42px;"," to the paragraph. The final step is to attach all the created elements to the shadow root using ",[22,3979,3980],{},"shadowRoot()",[27,3982,3983,3984,3987],{},"The actual updates are all handled by the life cycle callbacks, which are placed inside the class definition as methods. The ",[22,3985,3986],{},"connectedCallback()"," is called after the element is added to the DOM. In our case we added text to the paragraph we created.",[27,3989,3990,3991,3994],{},"Finally, we will register our custom element using the ",[22,3992,3993],{},"define()"," method. In the parameters, we specify the element name, followed by the class name that defines its functionality.",[3244,3996,3997],{},[27,3998,3999,4001,4002,164,4004,164,4007,4010,4011,4014,4015,1017],{},[42,4000,3250],{}," We can define a few different callbacks inside a custom element's class definition, which fire at different points in the element's lifecycle: ",[22,4003,3986],{},[22,4005,4006],{},"disconnectedCallback()",[22,4008,4009],{},"adoptedCallback()"," , ",[22,4012,4013],{},"attributeChangedCallback()",". For more details on using the lifecycle callbacks refer to this ",[45,4016,2726],{"href":4017,"target":2716,"rel":4018},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FWeb_Components\u002FUsing_custom_elements",[2718,2719],[27,4020,4021],{},"Our custom HTML element is now available to use on our page. We can use the custom element in our HTML like the example below:",[128,4023,4027],{"className":4024,"code":4025,"language":4026,"meta":133,"style":133},"language-html shiki shiki-themes github-light github-dark","\u003Cscript src=\"custom-element.js\">\u003C\u002Fscript>\n...\n\u003Cbody>\n    \u003Ccustom-element \u002F>\n\u003C\u002Fbody>\n","html",[22,4028,4029,4054,4059,4068,4079],{"__ignoreMap":133},[137,4030,4031,4034,4038,4041,4043,4046,4049,4051],{"class":139,"line":140},[137,4032,4033],{"class":157},"\u003C",[137,4035,4037],{"class":4036},"s9eBZ","script",[137,4039,4040],{"class":147}," src",[137,4042,253],{"class":157},[137,4044,4045],{"class":284},"\"custom-element.js\"",[137,4047,4048],{"class":157},">\u003C\u002F",[137,4050,4037],{"class":4036},[137,4052,4053],{"class":157},">\n",[137,4055,4056],{"class":139,"line":173},[137,4057,4058],{"class":157},"...\n",[137,4060,4061,4063,4066],{"class":139,"line":188},[137,4062,4033],{"class":157},[137,4064,4065],{"class":4036},"body",[137,4067,4053],{"class":157},[137,4069,4070,4073,4076],{"class":139,"line":269},[137,4071,4072],{"class":157},"    \u003C",[137,4074,4075],{"class":4036},"custom-element",[137,4077,4078],{"class":157}," \u002F>\n",[137,4080,4081,4084,4086],{"class":139,"line":278},[137,4082,4083],{"class":157},"\u003C\u002F",[137,4085,4065],{"class":4036},[137,4087,4053],{"class":157},[27,4089,4090],{},"This will be reflected in the browser:",[27,4092,4093],{},[63,4094],{"alt":65,"src":4095},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1627387526\u002Fblog\u002Fthis-is-a-paragraph",[27,4097,4098,4099,1017],{},"A full documentation on Web Components can be found on ",[45,4100,4103],{"href":4101,"target":2716,"rel":4102},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FWeb_Components",[2718,2719],"MDN Web Docs",[104,4105,4107],{"id":4106},"create-a-web-components-with-vuejs","Create a Web Components with Vue.js",[27,4109,4110],{},"The idea of using Vue.js to build a web component came whilst experimenting with some examples, as I reflected on how could I take advantage of Vue's power to create an efficient web component. It's not to say that we should include Vue.js in every web component we create, but it could be useful in certain cases. It is important to note that Vue.js adds some weight to your component, which might in turn affect your loading speed.",[27,4112,4113,4114,4117],{},"We are going to start by defining a class called ",[22,4115,4116],{},"UserRegister",", which extends the generic HTMLElement class.",[128,4119,4121],{"className":130,"code":4120,"language":132,"meta":133,"style":133},"class UserRegister extends HTMLElement {\n  constructor() {\n    super();\n\n    this.attachShadow({ mode: \"open\" });\n\n    const scriptVue = document.createElement(\"script\");\n    scriptVue.src = \"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fvue@2\";\n\n    const mainApp = document.createElement(\"div\");\n    mainApp.id = \"app\";\n\n    const componentStyle = document.createElement(\"style\");\n\n    this.shadowRoot.append(scriptVue, mainApp, componentStyle);\n  }\n",[22,4122,4123,4136,4143,4150,4154,4169,4173,4194,4206,4210,4230,4242,4246,4265,4269,4280],{"__ignoreMap":133},[137,4124,4125,4127,4130,4132,4134],{"class":139,"line":140},[137,4126,3635],{"class":143},[137,4128,4129],{"class":147}," UserRegister",[137,4131,3641],{"class":143},[137,4133,3644],{"class":147},[137,4135,256],{"class":157},[137,4137,4138,4141],{"class":139,"line":173},[137,4139,4140],{"class":143},"  constructor",[137,4142,275],{"class":157},[137,4144,4145,4148],{"class":139,"line":188},[137,4146,4147],{"class":364},"    super",[137,4149,924],{"class":157},[137,4151,4152],{"class":139,"line":269},[137,4153,516],{"emptyLinePlaceholder":515},[137,4155,4156,4158,4160,4162,4164,4166],{"class":139,"line":278},[137,4157,1394],{"class":364},[137,4159,1017],{"class":157},[137,4161,3684],{"class":147},[137,4163,3687],{"class":157},[137,4165,3690],{"class":284},[137,4167,4168],{"class":157}," });\n",[137,4170,4171],{"class":139,"line":291},[137,4172,516],{"emptyLinePlaceholder":515},[137,4174,4175,4178,4181,4183,4185,4187,4189,4192],{"class":139,"line":297},[137,4176,4177],{"class":143},"    const",[137,4179,4180],{"class":364}," scriptVue",[137,4182,151],{"class":143},[137,4184,3717],{"class":157},[137,4186,3720],{"class":147},[137,4188,356],{"class":157},[137,4190,4191],{"class":284},"\"script\"",[137,4193,1502],{"class":157},[137,4195,4196,4199,4201,4204],{"class":139,"line":302},[137,4197,4198],{"class":157},"    scriptVue.src ",[137,4200,253],{"class":143},[137,4202,4203],{"class":284}," \"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fvue@2\"",[137,4205,3276],{"class":157},[137,4207,4208],{"class":139,"line":662},[137,4209,516],{"emptyLinePlaceholder":515},[137,4211,4212,4214,4217,4219,4221,4223,4225,4228],{"class":139,"line":667},[137,4213,4177],{"class":143},[137,4215,4216],{"class":364}," mainApp",[137,4218,151],{"class":143},[137,4220,3717],{"class":157},[137,4222,3720],{"class":147},[137,4224,356],{"class":157},[137,4226,4227],{"class":284},"\"div\"",[137,4229,1502],{"class":157},[137,4231,4232,4235,4237,4240],{"class":139,"line":786},[137,4233,4234],{"class":157},"    mainApp.id ",[137,4236,253],{"class":143},[137,4238,4239],{"class":284}," \"app\"",[137,4241,3276],{"class":157},[137,4243,4244],{"class":139,"line":798},[137,4245,516],{"emptyLinePlaceholder":515},[137,4247,4248,4250,4253,4255,4257,4259,4261,4263],{"class":139,"line":803},[137,4249,4177],{"class":143},[137,4251,4252],{"class":364}," componentStyle",[137,4254,151],{"class":143},[137,4256,3717],{"class":157},[137,4258,3720],{"class":147},[137,4260,356],{"class":157},[137,4262,3766],{"class":284},[137,4264,1502],{"class":157},[137,4266,4267],{"class":139,"line":931},[137,4268,516],{"emptyLinePlaceholder":515},[137,4270,4271,4273,4275,4277],{"class":139,"line":1568},[137,4272,1394],{"class":364},[137,4274,3813],{"class":157},[137,4276,3816],{"class":147},[137,4278,4279],{"class":157},"(scriptVue, mainApp, componentStyle);\n",[137,4281,4282],{"class":139,"line":1573},[137,4283,3462],{"class":157},[27,4285,4286,4287,4289,4290,4292,4293,4296,4297,4299,4300,3969,4303,4305,4306,1017],{},"In the ",[22,4288,3939],{}," methods we attach the shadow root ",[22,4291,3946],{}," so that we can encapsulate our component from the rest of the document. Next, we create a ",[22,4294,4295],{},"\u003Cscript>"," element where we import Vue.js from CDN. We also create ",[22,4298,3595],{}," with an id of \"",[42,4301,4302],{},"app",[22,4304,3972],{}," element. Lastly we attach all the created elements to the shadow root using ",[22,4307,3980],{},[27,4309,4310,4311,4313,4314,4316],{},"When our ",[22,4312,3609],{}," component is added to the DOM, we should initialise Vue.js and add some styles. We will accomplish that above by calling ",[22,4315,3986],{}," in our component.",[128,4318,4320],{"className":130,"code":4319,"language":132,"meta":133,"style":133},"connectedCallback() {\n    const shadow = this.shadowRoot;\n    this.createVue(shadow);\n    this.updateStyle(shadow);\n  }\n",[22,4321,4322,4329,4341,4353,4364],{"__ignoreMap":133},[137,4323,4324,4327],{"class":139,"line":140},[137,4325,4326],{"class":147},"connectedCallback",[137,4328,275],{"class":157},[137,4330,4331,4333,4335,4337,4339],{"class":139,"line":173},[137,4332,4177],{"class":143},[137,4334,3851],{"class":364},[137,4336,151],{"class":143},[137,4338,365],{"class":364},[137,4340,3858],{"class":157},[137,4342,4343,4345,4347,4350],{"class":139,"line":188},[137,4344,1394],{"class":364},[137,4346,1017],{"class":157},[137,4348,4349],{"class":147},"createVue",[137,4351,4352],{"class":157},"(shadow);\n",[137,4354,4355,4357,4359,4362],{"class":139,"line":269},[137,4356,1394],{"class":364},[137,4358,1017],{"class":157},[137,4360,4361],{"class":147},"updateStyle",[137,4363,4352],{"class":157},[137,4365,4366],{"class":139,"line":278},[137,4367,3462],{"class":157},[27,4369,4370,4371,4373,4374,114,4377,4380,4381,4383],{},"Inside ",[22,4372,3986],{}," we call ",[22,4375,4376],{},"createVue()",[22,4378,4379],{},"updateStyle()"," methods, passing the shadow root parameter. We define these methods inside ",[22,4382,4116],{}," class.",[128,4385,4387],{"className":130,"code":4386,"language":132,"meta":133,"style":133},"createVue(shadow) {\n    shadow.querySelector(\"script\").addEventListener(\"load\", () => {\n      const template = `\n      \u003Cform class=\"card\" @submit.prevent=\"submit\">\n        \u003Ch2 class=\"text-blue\">Register Form\u003C\u002Fh2>\n        \u003Cp>This is a custom widget Web Component made with Vue.js.\u003C\u002Fp>\n        \u003Clabel class=\"label\" for=\"first\">\u003Cstrong>First Name\u003C\u002Fstrong>\u003C\u002Flabel>\n        \u003Cinput v-model=\"firstName\" class=\"input\" id=\"first\" type=\"text\">\n        \u003Clabel class=\"label\" for=\"last\">\u003Cstrong>Last Name\u003C\u002Fstrong>\u003C\u002Flabel>\n        \u003Cinput v-model=\"lastName\" class=\"input\" id=\"last\" type=\"text\">\n        \u003Cbutton class=\"btn\" type=\"submit\">Register\u003C\u002Fbutton>\n        \u003Cp class=\"message\">{{ message }}\u003C\u002Fp>\n      \u003C\u002Fform>`;\n\n      new Vue({\n        el: shadow.querySelector(\"#app\"),\n        template,\n        data: {\n          firstName: \"\",\n          lastName: \"\",\n          message: \"\",\n        },\n        methods: {\n          submit() {\n            this.message = `User with name: ${this.fullName} has been registered!`;\n            this.firstName = \"\";\n            this.lastName = \"\";\n            setTimeout(() => {\n              this.message = \"\";\n            }, 6000);\n          },\n        },\n        computed: {\n          fullName() {\n            return `${this.firstName} ${this.lastName}`;\n          },\n        },\n      });\n    });\n  }\n",[22,4388,4389,4396,4425,4437,4442,4447,4452,4457,4462,4467,4472,4477,4482,4489,4493,4503,4518,4523,4528,4538,4547,4556,4560,4565,4572,4597,4610,4622,4632,4645,4655,4660,4664,4670,4678,4709,4714,4719,4725,4730],{"__ignoreMap":133},[137,4390,4391,4393],{"class":139,"line":140},[137,4392,4349],{"class":147},[137,4394,4395],{"class":157},"(shadow) {\n",[137,4397,4398,4401,4403,4405,4407,4410,4413,4415,4418,4421,4423],{"class":139,"line":173},[137,4399,4400],{"class":157},"    shadow.",[137,4402,3873],{"class":147},[137,4404,356],{"class":157},[137,4406,4191],{"class":284},[137,4408,4409],{"class":157},").",[137,4411,4412],{"class":147},"addEventListener",[137,4414,356],{"class":157},[137,4416,4417],{"class":284},"\"load\"",[137,4419,4420],{"class":157},", () ",[137,4422,222],{"class":143},[137,4424,256],{"class":157},[137,4426,4427,4430,4433,4435],{"class":139,"line":188},[137,4428,4429],{"class":143},"      const",[137,4431,4432],{"class":364}," template",[137,4434,151],{"class":143},[137,4436,3778],{"class":284},[137,4438,4439],{"class":139,"line":269},[137,4440,4441],{"class":284},"      \u003Cform class=\"card\" @submit.prevent=\"submit\">\n",[137,4443,4444],{"class":139,"line":278},[137,4445,4446],{"class":284},"        \u003Ch2 class=\"text-blue\">Register Form\u003C\u002Fh2>\n",[137,4448,4449],{"class":139,"line":291},[137,4450,4451],{"class":284},"        \u003Cp>This is a custom widget Web Component made with Vue.js.\u003C\u002Fp>\n",[137,4453,4454],{"class":139,"line":297},[137,4455,4456],{"class":284},"        \u003Clabel class=\"label\" for=\"first\">\u003Cstrong>First Name\u003C\u002Fstrong>\u003C\u002Flabel>\n",[137,4458,4459],{"class":139,"line":302},[137,4460,4461],{"class":284},"        \u003Cinput v-model=\"firstName\" class=\"input\" id=\"first\" type=\"text\">\n",[137,4463,4464],{"class":139,"line":662},[137,4465,4466],{"class":284},"        \u003Clabel class=\"label\" for=\"last\">\u003Cstrong>Last Name\u003C\u002Fstrong>\u003C\u002Flabel>\n",[137,4468,4469],{"class":139,"line":667},[137,4470,4471],{"class":284},"        \u003Cinput v-model=\"lastName\" class=\"input\" id=\"last\" type=\"text\">\n",[137,4473,4474],{"class":139,"line":786},[137,4475,4476],{"class":284},"        \u003Cbutton class=\"btn\" type=\"submit\">Register\u003C\u002Fbutton>\n",[137,4478,4479],{"class":139,"line":798},[137,4480,4481],{"class":284},"        \u003Cp class=\"message\">{{ message }}\u003C\u002Fp>\n",[137,4483,4484,4487],{"class":139,"line":803},[137,4485,4486],{"class":284},"      \u003C\u002Fform>`",[137,4488,3276],{"class":157},[137,4490,4491],{"class":139,"line":931},[137,4492,516],{"emptyLinePlaceholder":515},[137,4494,4495,4498,4501],{"class":139,"line":1568},[137,4496,4497],{"class":143},"      new",[137,4499,4500],{"class":147}," Vue",[137,4502,3175],{"class":157},[137,4504,4505,4508,4510,4512,4515],{"class":139,"line":1573},[137,4506,4507],{"class":157},"        el: shadow.",[137,4509,3873],{"class":147},[137,4511,356],{"class":157},[137,4513,4514],{"class":284},"\"#app\"",[137,4516,4517],{"class":157},"),\n",[137,4519,4520],{"class":139,"line":1578},[137,4521,4522],{"class":157},"        template,\n",[137,4524,4525],{"class":139,"line":1588},[137,4526,4527],{"class":157},"        data: {\n",[137,4529,4530,4533,4536],{"class":139,"line":1601},[137,4531,4532],{"class":157},"          firstName: ",[137,4534,4535],{"class":284},"\"\"",[137,4537,1961],{"class":157},[137,4539,4540,4543,4545],{"class":139,"line":3802},[137,4541,4542],{"class":157},"          lastName: ",[137,4544,4535],{"class":284},[137,4546,1961],{"class":157},[137,4548,4549,4552,4554],{"class":139,"line":3808},[137,4550,4551],{"class":157},"          message: ",[137,4553,4535],{"class":284},[137,4555,1961],{"class":157},[137,4557,4558],{"class":139,"line":3822},[137,4559,2084],{"class":157},[137,4561,4562],{"class":139,"line":3827},[137,4563,4564],{"class":157},"        methods: {\n",[137,4566,4567,4570],{"class":139,"line":3832},[137,4568,4569],{"class":147},"          submit",[137,4571,275],{"class":157},[137,4573,4574,4577,4580,4582,4585,4587,4589,4592,4595],{"class":139,"line":3840},[137,4575,4576],{"class":364},"            this",[137,4578,4579],{"class":157},".message ",[137,4581,253],{"class":143},[137,4583,4584],{"class":284}," `User with name: ${",[137,4586,24],{"class":364},[137,4588,1017],{"class":284},[137,4590,4591],{"class":157},"fullName",[137,4593,4594],{"class":284},"} has been registered!`",[137,4596,3276],{"class":157},[137,4598,4599,4601,4603,4605,4608],{"class":139,"line":3846},[137,4600,4576],{"class":364},[137,4602,368],{"class":157},[137,4604,253],{"class":143},[137,4606,4607],{"class":284}," \"\"",[137,4609,3276],{"class":157},[137,4611,4612,4614,4616,4618,4620],{"class":139,"line":3861},[137,4613,4576],{"class":364},[137,4615,1085],{"class":157},[137,4617,253],{"class":143},[137,4619,4607],{"class":284},[137,4621,3276],{"class":157},[137,4623,4624,4626,4628,4630],{"class":139,"line":3883},[137,4625,2067],{"class":147},[137,4627,3193],{"class":157},[137,4629,222],{"class":143},[137,4631,256],{"class":157},[137,4633,4634,4637,4639,4641,4643],{"class":139,"line":3896},[137,4635,4636],{"class":364},"              this",[137,4638,4579],{"class":157},[137,4640,253],{"class":143},[137,4642,4607],{"class":284},[137,4644,3276],{"class":157},[137,4646,4647,4650,4653],{"class":139,"line":3901},[137,4648,4649],{"class":157},"            }, ",[137,4651,4652],{"class":364},"6000",[137,4654,1502],{"class":157},[137,4656,4657],{"class":139,"line":3906},[137,4658,4659],{"class":157},"          },\n",[137,4661,4662],{"class":139,"line":3911},[137,4663,2084],{"class":157},[137,4665,4667],{"class":139,"line":4666},33,[137,4668,4669],{"class":157},"        computed: {\n",[137,4671,4673,4676],{"class":139,"line":4672},34,[137,4674,4675],{"class":147},"          fullName",[137,4677,275],{"class":157},[137,4679,4681,4684,4687,4689,4691,4694,4697,4699,4701,4704,4707],{"class":139,"line":4680},35,[137,4682,4683],{"class":143},"            return",[137,4685,4686],{"class":284}," `${",[137,4688,24],{"class":364},[137,4690,1017],{"class":284},[137,4692,4693],{"class":157},"firstName",[137,4695,4696],{"class":284},"} ${",[137,4698,24],{"class":364},[137,4700,1017],{"class":284},[137,4702,4703],{"class":157},"lastName",[137,4705,4706],{"class":284},"}`",[137,4708,3276],{"class":157},[137,4710,4712],{"class":139,"line":4711},36,[137,4713,4659],{"class":157},[137,4715,4717],{"class":139,"line":4716},37,[137,4718,2084],{"class":157},[137,4720,4722],{"class":139,"line":4721},38,[137,4723,4724],{"class":157},"      });\n",[137,4726,4728],{"class":139,"line":4727},39,[137,4729,2832],{"class":157},[137,4731,4733],{"class":139,"line":4732},40,[137,4734,3462],{"class":157},[27,4736,4737,4738,4740,4741,4743,4744,4746],{},"The ",[22,4739,4376],{}," method is where we define the Vue logic. First, we select the ",[22,4742,4295],{}," element that we have created in the ",[22,4745,3939],{}," method, and then wait the Vue.js script to be loaded. Once the script is loaded we continue with our Vue initialisation.",[27,4748,4749,4750,4752,4753,4755,4756,4758,4759,4762,4763,164,4765,114,4767,4770,4771,4774,4775,4778,4779,1017],{},"Here we write our template as we normally do in Vue. We then need to tell Vue to render our templet in the ",[22,4751,3595],{}," element with an id of \"",[42,4754,4302],{},"\" that we created earlier in the ",[22,4757,3939],{}," method. We do that with ",[22,4760,4761],{},"el: shadow.querySelector(\"#app\")",". We also add three reactive properties in the data object: ",[22,4764,4693],{},[22,4766,4703],{},[22,4768,4769],{},"message",", a ",[22,4772,4773],{},"submit()"," method as well as computed function ",[22,4776,4777],{},"fullName()",". For more information on how Vue.js works please refer to the Vue.js documentation in the following ",[45,4780,2726],{"href":4781,"target":2716,"rel":4782},"https:\u002F\u002Fvuejs.org\u002Fv2\u002Fguide\u002F",[2718,2719],[27,4784,4785,4786,4788,4789,4791,4792,4794],{},"Next, we add some styles in our component. In the ",[22,4787,4379],{}," method, we should first select the ",[22,4790,3972],{}," element that we created in the ",[22,4793,3939],{}," , and then set textContent by adding some CSS style.",[128,4796,4798],{"className":130,"code":4797,"language":132,"meta":133,"style":133},"updateStyle(shadow) {\n    shadow.querySelector(\"style\").textContent = `\n      .card {\n        padding: 16px 30px;\n        max-width: 600px;\n        box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);\n      }\n      .text-blue {\n        color: #2196f3;\n      }\n      .label {\n        display: inline-block;\n        margin-top: 8px;\n        color: #2196f3;\n      }\n      .input {\n        padding: 8px;\n        margin: 2px 0;\n        display: block;\n        border: 1px solid #ccc;\n        width: 100%;\n      }\n      .btn {\n        border: none;\n        display: inline-block;\n        margin-top: 8px;\n        padding: 8px 16px;\n        vertical-align: middle;\n        overflow: hidden;\n        text-decoration: none;\n        color: #fff;\n        background-color: #2196f3;\n        text-align: center;\n        cursor: pointer;\n        white-space: nowrap;\n      }\n      .message {\n        color: #2196f3;\n        font-weight: bold;\n        text-align: center;\n      }`;\n  }\n",[22,4799,4800,4806,4823,4828,4833,4838,4843,4848,4853,4858,4862,4867,4872,4877,4881,4885,4890,4895,4900,4905,4910,4915,4919,4924,4929,4933,4937,4942,4947,4952,4957,4962,4967,4972,4977,4982,4986,4991,4995,5000,5004,5012],{"__ignoreMap":133},[137,4801,4802,4804],{"class":139,"line":140},[137,4803,4361],{"class":147},[137,4805,4395],{"class":157},[137,4807,4808,4810,4812,4814,4816,4819,4821],{"class":139,"line":173},[137,4809,4400],{"class":157},[137,4811,3873],{"class":147},[137,4813,356],{"class":157},[137,4815,3766],{"class":284},[137,4817,4818],{"class":157},").textContent ",[137,4820,253],{"class":143},[137,4822,3778],{"class":284},[137,4824,4825],{"class":139,"line":188},[137,4826,4827],{"class":284},"      .card {\n",[137,4829,4830],{"class":139,"line":269},[137,4831,4832],{"class":284},"        padding: 16px 30px;\n",[137,4834,4835],{"class":139,"line":278},[137,4836,4837],{"class":284},"        max-width: 600px;\n",[137,4839,4840],{"class":139,"line":291},[137,4841,4842],{"class":284},"        box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);\n",[137,4844,4845],{"class":139,"line":297},[137,4846,4847],{"class":284},"      }\n",[137,4849,4850],{"class":139,"line":302},[137,4851,4852],{"class":284},"      .text-blue {\n",[137,4854,4855],{"class":139,"line":662},[137,4856,4857],{"class":284},"        color: #2196f3;\n",[137,4859,4860],{"class":139,"line":667},[137,4861,4847],{"class":284},[137,4863,4864],{"class":139,"line":786},[137,4865,4866],{"class":284},"      .label {\n",[137,4868,4869],{"class":139,"line":798},[137,4870,4871],{"class":284},"        display: inline-block;\n",[137,4873,4874],{"class":139,"line":803},[137,4875,4876],{"class":284},"        margin-top: 8px;\n",[137,4878,4879],{"class":139,"line":931},[137,4880,4857],{"class":284},[137,4882,4883],{"class":139,"line":1568},[137,4884,4847],{"class":284},[137,4886,4887],{"class":139,"line":1573},[137,4888,4889],{"class":284},"      .input {\n",[137,4891,4892],{"class":139,"line":1578},[137,4893,4894],{"class":284},"        padding: 8px;\n",[137,4896,4897],{"class":139,"line":1588},[137,4898,4899],{"class":284},"        margin: 2px 0;\n",[137,4901,4902],{"class":139,"line":1601},[137,4903,4904],{"class":284},"        display: block;\n",[137,4906,4907],{"class":139,"line":3802},[137,4908,4909],{"class":284},"        border: 1px solid #ccc;\n",[137,4911,4912],{"class":139,"line":3808},[137,4913,4914],{"class":284},"        width: 100%;\n",[137,4916,4917],{"class":139,"line":3822},[137,4918,4847],{"class":284},[137,4920,4921],{"class":139,"line":3827},[137,4922,4923],{"class":284},"      .btn {\n",[137,4925,4926],{"class":139,"line":3832},[137,4927,4928],{"class":284},"        border: none;\n",[137,4930,4931],{"class":139,"line":3840},[137,4932,4871],{"class":284},[137,4934,4935],{"class":139,"line":3846},[137,4936,4876],{"class":284},[137,4938,4939],{"class":139,"line":3861},[137,4940,4941],{"class":284},"        padding: 8px 16px;\n",[137,4943,4944],{"class":139,"line":3883},[137,4945,4946],{"class":284},"        vertical-align: middle;\n",[137,4948,4949],{"class":139,"line":3896},[137,4950,4951],{"class":284},"        overflow: hidden;\n",[137,4953,4954],{"class":139,"line":3901},[137,4955,4956],{"class":284},"        text-decoration: none;\n",[137,4958,4959],{"class":139,"line":3906},[137,4960,4961],{"class":284},"        color: #fff;\n",[137,4963,4964],{"class":139,"line":3911},[137,4965,4966],{"class":284},"        background-color: #2196f3;\n",[137,4968,4969],{"class":139,"line":4666},[137,4970,4971],{"class":284},"        text-align: center;\n",[137,4973,4974],{"class":139,"line":4672},[137,4975,4976],{"class":284},"        cursor: pointer;\n",[137,4978,4979],{"class":139,"line":4680},[137,4980,4981],{"class":284},"        white-space: nowrap;\n",[137,4983,4984],{"class":139,"line":4711},[137,4985,4847],{"class":284},[137,4987,4988],{"class":139,"line":4716},[137,4989,4990],{"class":284},"      .message {\n",[137,4992,4993],{"class":139,"line":4721},[137,4994,4857],{"class":284},[137,4996,4997],{"class":139,"line":4727},[137,4998,4999],{"class":284},"        font-weight: bold;\n",[137,5001,5002],{"class":139,"line":4732},[137,5003,4971],{"class":284},[137,5005,5007,5010],{"class":139,"line":5006},41,[137,5008,5009],{"class":284},"      }`",[137,5011,3276],{"class":157},[137,5013,5015],{"class":139,"line":5014},42,[137,5016,3462],{"class":157},[27,5018,5019,5020,5022],{},"Finally, we register our custom element using the ",[22,5021,3993],{}," method. In the parameters, we specify the element name, and then the class name that defines its functionality.",[128,5024,5026],{"className":130,"code":5025,"language":132,"meta":133,"style":133},"customElements.define(\"user-register\", UserRegister);\n",[22,5027,5028],{"__ignoreMap":133},[137,5029,5030,5032,5034,5036,5039],{"class":139,"line":140},[137,5031,3914],{"class":157},[137,5033,3917],{"class":147},[137,5035,356],{"class":157},[137,5037,5038],{"class":284},"\"user-register\"",[137,5040,5041],{"class":157},", UserRegister);\n",[27,5043,5044,5045,5047],{},"That's it! Our custom ",[22,5046,3609],{}," element is now ready to be used on our page. We can use it in our HTML like the example below:",[128,5049,5051],{"className":4024,"code":5050,"language":4026,"meta":133,"style":133},"\u003Cscript src=\"user-register.js\">\u003C\u002Fscript>\n...\n\u003Cbody>\n    \u003Cuser-register \u002F>\n\u003C\u002Fbody>\n",[22,5052,5053,5072,5076,5084,5093],{"__ignoreMap":133},[137,5054,5055,5057,5059,5061,5063,5066,5068,5070],{"class":139,"line":140},[137,5056,4033],{"class":157},[137,5058,4037],{"class":4036},[137,5060,4040],{"class":147},[137,5062,253],{"class":157},[137,5064,5065],{"class":284},"\"user-register.js\"",[137,5067,4048],{"class":157},[137,5069,4037],{"class":4036},[137,5071,4053],{"class":157},[137,5073,5074],{"class":139,"line":173},[137,5075,4058],{"class":157},[137,5077,5078,5080,5082],{"class":139,"line":188},[137,5079,4033],{"class":157},[137,5081,4065],{"class":4036},[137,5083,4053],{"class":157},[137,5085,5086,5088,5091],{"class":139,"line":269},[137,5087,4072],{"class":157},[137,5089,5090],{"class":4036},"user-register",[137,5092,4078],{"class":157},[137,5094,5095,5097,5099],{"class":139,"line":278},[137,5096,4083],{"class":157},[137,5098,4065],{"class":4036},[137,5100,4053],{"class":157},[3244,5102,5103],{},[27,5104,5105,5107,5108,5110,5111,164,5114,5110,5116,164,5119,5110,5121,114,5124,5110,5126,1017],{},[42,5106,3250],{}," Now when we have created a Web Components with Vue.js, how can we compare the Web Components lifecycles to Vue.js lifecycles hooks: ",[22,5109,3939],{}," = ",[22,5112,5113],{},"created()",[22,5115,3986],{},[22,5117,5118],{},"mounted()",[22,5120,4013],{},[22,5122,5123],{},"beforeUpdate()",[22,5125,4006],{},[22,5127,5128],{},"destroyed()",[104,5130,5132],{"id":5131},"create-web-component-with-vue-cli","Create Web Component with Vue-CLI",[27,5134,5135,5136,5141,5142,1017],{},"Vue-CLI version 3 and above allow for us to export our Vue.js components into Web Components. All we're required to do is to add a build target in ",[42,5137,5138],{},[22,5139,5140],{},"package.json"," file. More details on building targets can be found in the following ",[45,5143,2726],{"href":5144,"target":2716,"rel":5145},"https:\u002F\u002Fcli.vuejs.org\u002Fguide\u002Fbuild-targets.html#library",[2718,2719],[27,5147,4286,5148,5152],{},[42,5149,5150],{},[22,5151,5140],{}," we add the following line of code:",[128,5154,5158],{"className":5155,"code":5156,"language":5157,"meta":133,"style":133},"language-json shiki shiki-themes github-light github-dark","\"scripts\": {\n    \"wc\": \"vue-cli-service build --target wc --inline-vue --name custom-component .\u002Fsrc\u002Fcomponents\u002Fcustom-component.vue\"\n}\n","json",[22,5159,5160,5167,5177],{"__ignoreMap":133},[137,5161,5162,5165],{"class":139,"line":140},[137,5163,5164],{"class":284},"\"scripts\"",[137,5166,1819],{"class":157},[137,5168,5169,5172,5174],{"class":139,"line":173},[137,5170,5171],{"class":364},"    \"wc\"",[137,5173,726],{"class":157},[137,5175,5176],{"class":284},"\"vue-cli-service build --target wc --inline-vue --name custom-component .\u002Fsrc\u002Fcomponents\u002Fcustom-component.vue\"\n",[137,5178,5179],{"class":139,"line":188},[137,5180,510],{"class":157},[27,5182,5183],{},"And then we run the following script in the console:",[128,5185,5190],{"className":5186,"code":5188,"language":5189},[5187],"language-text","npm run wc\n","text",[22,5191,5188],{"__ignoreMap":133},[27,5193,5194],{},"The heavy work is already done by Vue-CLI. We now can use our custom component in our HTML like the example below:",[128,5196,5198],{"className":4024,"code":5197,"language":4026,"meta":133,"style":133},"\u003Cscript src=\"custom-component.js\">\u003C\u002Fscript>\n...\n\u003Cbody>\n    \u003Ccustom-component \u002F>\n\u003C\u002Fbody>\n",[22,5199,5200,5219,5223,5231,5240],{"__ignoreMap":133},[137,5201,5202,5204,5206,5208,5210,5213,5215,5217],{"class":139,"line":140},[137,5203,4033],{"class":157},[137,5205,4037],{"class":4036},[137,5207,4040],{"class":147},[137,5209,253],{"class":157},[137,5211,5212],{"class":284},"\"custom-component.js\"",[137,5214,4048],{"class":157},[137,5216,4037],{"class":4036},[137,5218,4053],{"class":157},[137,5220,5221],{"class":139,"line":173},[137,5222,4058],{"class":157},[137,5224,5225,5227,5229],{"class":139,"line":188},[137,5226,4033],{"class":157},[137,5228,4065],{"class":4036},[137,5230,4053],{"class":157},[137,5232,5233,5235,5238],{"class":139,"line":269},[137,5234,4072],{"class":157},[137,5236,5237],{"class":4036},"custom-component",[137,5239,4078],{"class":157},[137,5241,5242,5244,5246],{"class":139,"line":278},[137,5243,4083],{"class":157},[137,5245,4065],{"class":4036},[137,5247,4053],{"class":157},[104,5249,2567],{"id":2566},[2569,5251,5252,5255,5265,5268,5271],{},[1006,5253,5254],{},"Web components are a set of web platform APIs that allow us to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps.",[1006,5256,5257,5258,3596,5260,3596,5262,5264],{},"Web Components are native HTML elements such as ",[22,5259,3595],{},[22,5261,3599],{},[22,5263,3602],{},", but something that we can create ourselves with encapsulated functionality that can be reused across modern web browsers.",[1006,5266,5267],{},"An important aspect of web components is encapsulation. The Shadow DOM API is a key part of this, providing a way to attach a hidden separated DOM to an element.",[1006,5269,5270],{},"We can take advantage of Vue's power to create efficient web components. However, we should be aware of it's size, as it affects the loading speed of the component.",[1006,5272,5273],{},"The Vue-CLI version 3 and above allow us to export our Vue.js components into Web Components. The heavy lifting would be carried out by Vue-CLI.",[27,5275,5276,5277,1017],{},"All examples above can be found in the following github repository ",[45,5278,2726],{"href":5279,"target":2716,"rel":5280},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002FWeb-Components-and-Vue.js",[2718,2719],[2617,5282,5283],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":133,"searchDepth":173,"depth":173,"links":5285},[5286,5287,5288,5289,5290],{"id":3588,"depth":173,"text":3589},{"id":3621,"depth":173,"text":3622},{"id":4106,"depth":173,"text":4107},{"id":5131,"depth":173,"text":5132},{"id":2566,"depth":173,"text":2567},"In this day and age writing reusable components that can be used multiple times in your application or shared between other web platforms can actually save you a lot of time and effort. Web Components offer this benefit and makes it easier to build custom complex components. At the same time, this improves its reusability. Web components are a set of web platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps. Think native HTML elements \u003Cdiv>, \u003Csection>, \u003Cbutton>, but something that we can create ourselves with encapsulated functionality that can be reused across modern web browsers or any JavaScript library or framework that works with HTML.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1627371334\u002Fblog\u002Fcreate-a-custom-HTML-element-using-Web-Components-and-Vuejs",[5294,5295,5296,5297,8,5298,2653,5299,5300],"Custom HTML element","Custom Elements","Web Components","Web Components and Vue.js","Vue CLI","Development","Web Development",{},"\u002F2021\u002F07\u002F24\u002Fcreate-a-custom-html-element-using-web-components-and-vuejs","27th Jul 2021",{"title":3551,"description":5291},"2021\u002F07\u002F24\u002Fcreate-a-custom-html-element-using-web-components-and-vuejs","itt4heH5GCjgk9qhfPPmkWUVe7Ii83NUDBshZSRgvjQ",{"id":5308,"title":5309,"articleTags":5310,"author":11,"blog":12,"body":5311,"description":7514,"extension":2649,"image":7515,"keywords":7516,"meta":7521,"navigation":515,"path":7522,"published":7523,"readTime":291,"seo":7524,"stem":7525,"type":2662,"__hash__":7526},"content\u002F2021\u002F09\u002F07\u002Fwatch-javascript-variables-for-change.md","Watch JavaScript Variables for Change",[9,10,2669],{"type":14,"value":5312,"toc":7508},[5313,5316,5330,5332,5336,5341,5354,5372,5423,5441,5628,5631,5653,5656,5660,5663,5896,5902,5905,6222,6224,6243,6254,6258,6261,6264,6607,6609,6617,6632,6635,6962,6966,6987,7412,7414,7472,7474,7499,7505],[17,5314,5309],{"id":5315},"watch-javascript-variables-for-change",[27,5317,5318],{},[30,5319,5320,36,5322,40,5324],{},[33,5321],{"value":35},[33,5323],{"value":39},[42,5325,5326],{},[45,5327,5328],{"href":47},[33,5329],{"value":50},[52,5331],{":tags":54},[56,5333],{":audio-src":5334,":transcript-src":5335},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F09\u002F07\u002Fwatch-javascript-variables-for-change\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F09\u002F07\u002Fwatch-javascript-variables-for-change\u002Fsummary.json",[27,5337,5338],{},[63,5339],{"alt":65,"src":5340},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1630971440\u002Fblog\u002Fwatch-javascript-variables-for-change",[27,5342,5343,5344,114,5349,5353],{},"We all know that in JavaScript there is no event that fires when a value of a variable changes. But by defining ",[45,5345,5348],{"href":5346,"target":2716,"rel":5347},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FJavaScript\u002FGuide\u002FWorking_with_Objects#defining_getters_and_setters",[2718,2719],"getters",[45,5350,5352],{"href":5346,"target":2716,"rel":5351},[2718,2719],"setters"," in the object this is now possible.",[27,5355,5356,5357,3596,5360,5365,5366,5368,5369,5371],{},"First, define a new property on an object with ",[22,5358,5359],{},"Object.defineProperty",[45,5361,5364],{"href":5362,"target":2716,"rel":5363},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FJavaScript\u002FReference\u002FGlobal_Objects\u002FObject\u002FdefineProperty",[2718,2719],"method",". As a first argument, we pass the object on which to define the property. If the object is defined in the global scope, ",[22,5367,24],{}," will refer to the window object. In the second argument, we will define the name of the property. In our case, we define a property with name ",[22,5370,1387],{},". The third argument is the descriptor for the property being defined.",[128,5373,5375],{"className":130,"code":5374,"language":132,"meta":133,"style":133},"Object.defineProperty(this, \"name\", {\n    get() {},\n    set(value) {},\n});\n",[22,5376,5377,5397,5405,5418],{"__ignoreMap":133},[137,5378,5379,5382,5385,5387,5389,5391,5394],{"class":139,"line":140},[137,5380,5381],{"class":157},"Object.",[137,5383,5384],{"class":147},"defineProperty",[137,5386,356],{"class":157},[137,5388,24],{"class":364},[137,5390,164],{"class":157},[137,5392,5393],{"class":284},"\"name\"",[137,5395,5396],{"class":157},", {\n",[137,5398,5399,5402],{"class":139,"line":173},[137,5400,5401],{"class":147},"    get",[137,5403,5404],{"class":157},"() {},\n",[137,5406,5407,5410,5412,5415],{"class":139,"line":188},[137,5408,5409],{"class":147},"    set",[137,5411,356],{"class":157},[137,5413,5414],{"class":161},"value",[137,5416,5417],{"class":157},") {},\n",[137,5419,5420],{"class":139,"line":269},[137,5421,5422],{"class":157},"});\n",[27,5424,5425,5426,5429,5430,5433,5434,5437,5438,5440],{},"In the descriptor we write the logic in the getter and setting methods. In the setter if the received value is the same as the previous set value, we ",[22,5427,5428],{},"return",". Otherwise, we store the previous value in a variable called ",[22,5431,5432],{},"oldValue"," and we assign the new value in the variable ",[22,5435,5436],{},"_name",". We next return the ",[22,5439,5436],{}," in the getter.",[128,5442,5444],{"className":130,"code":5443,"language":132,"meta":133,"style":133},"Object.defineProperty(this, \"name\", {\n    get() {\n        return this._name;\n    },\n    set(value) {\n        if (value === this.name) {\n            return;\n        }\n        const oldValue = this.name;\n        this._name = value;\n        console.log(`Changed value from ${oldValue} to ${value}`);\n    },\n});\n\nthis.name = \"Aleks\";\nthis.name = \"Nicole\";\nthis.name = \"Nicole\";\nconsole.log(this.name);\n",[22,5445,5446,5462,5468,5478,5482,5492,5508,5514,5518,5532,5544,5566,5570,5574,5578,5591,5604,5616],{"__ignoreMap":133},[137,5447,5448,5450,5452,5454,5456,5458,5460],{"class":139,"line":140},[137,5449,5381],{"class":157},[137,5451,5384],{"class":147},[137,5453,356],{"class":157},[137,5455,24],{"class":364},[137,5457,164],{"class":157},[137,5459,5393],{"class":284},[137,5461,5396],{"class":157},[137,5463,5464,5466],{"class":139,"line":173},[137,5465,5401],{"class":147},[137,5467,275],{"class":157},[137,5469,5470,5473,5475],{"class":139,"line":188},[137,5471,5472],{"class":143},"        return",[137,5474,365],{"class":364},[137,5476,5477],{"class":157},"._name;\n",[137,5479,5480],{"class":139,"line":269},[137,5481,775],{"class":157},[137,5483,5484,5486,5488,5490],{"class":139,"line":278},[137,5485,5409],{"class":147},[137,5487,356],{"class":157},[137,5489,5414],{"class":161},[137,5491,170],{"class":157},[137,5493,5494,5497,5500,5503,5505],{"class":139,"line":291},[137,5495,5496],{"class":143},"        if",[137,5498,5499],{"class":157}," (value ",[137,5501,5502],{"class":143},"===",[137,5504,365],{"class":364},[137,5506,5507],{"class":157},".name) {\n",[137,5509,5510,5512],{"class":139,"line":297},[137,5511,4683],{"class":143},[137,5513,3276],{"class":157},[137,5515,5516],{"class":139,"line":302},[137,5517,1966],{"class":157},[137,5519,5520,5522,5525,5527,5529],{"class":139,"line":662},[137,5521,3008],{"class":143},[137,5523,5524],{"class":364}," oldValue",[137,5526,151],{"class":143},[137,5528,365],{"class":364},[137,5530,5531],{"class":157},".name;\n",[137,5533,5534,5536,5539,5541],{"class":139,"line":667},[137,5535,3679],{"class":364},[137,5537,5538],{"class":157},"._name ",[137,5540,253],{"class":143},[137,5542,5543],{"class":157}," value;\n",[137,5545,5546,5548,5550,5552,5555,5557,5560,5562,5564],{"class":139,"line":786},[137,5547,350],{"class":157},[137,5549,353],{"class":147},[137,5551,356],{"class":157},[137,5553,5554],{"class":284},"`Changed value from ${",[137,5556,5432],{"class":157},[137,5558,5559],{"class":284},"} to ${",[137,5561,5414],{"class":157},[137,5563,4706],{"class":284},[137,5565,1502],{"class":157},[137,5567,5568],{"class":139,"line":798},[137,5569,775],{"class":157},[137,5571,5572],{"class":139,"line":803},[137,5573,5422],{"class":157},[137,5575,5576],{"class":139,"line":931},[137,5577,516],{"emptyLinePlaceholder":515},[137,5579,5580,5582,5584,5586,5589],{"class":139,"line":1568},[137,5581,24],{"class":364},[137,5583,1397],{"class":157},[137,5585,253],{"class":143},[137,5587,5588],{"class":284}," \"Aleks\"",[137,5590,3276],{"class":157},[137,5592,5593,5595,5597,5599,5602],{"class":139,"line":1573},[137,5594,24],{"class":364},[137,5596,1397],{"class":157},[137,5598,253],{"class":143},[137,5600,5601],{"class":284}," \"Nicole\"",[137,5603,3276],{"class":157},[137,5605,5606,5608,5610,5612,5614],{"class":139,"line":1578},[137,5607,24],{"class":364},[137,5609,1397],{"class":157},[137,5611,253],{"class":143},[137,5613,5601],{"class":284},[137,5615,3276],{"class":157},[137,5617,5618,5620,5622,5624,5626],{"class":139,"line":1588},[137,5619,1436],{"class":157},[137,5621,353],{"class":147},[137,5623,356],{"class":157},[137,5625,24],{"class":364},[137,5627,505],{"class":157},[27,5629,5630],{},"The output of this will be:",[128,5632,5636],{"className":5633,"code":5634,"language":5635,"meta":133,"style":133},"language-plain shiki shiki-themes github-light github-dark","Changed value from undefined to Aleks\nChanged value from Aleks to Nicole\nNicole\n","plain",[22,5637,5638,5643,5648],{"__ignoreMap":133},[137,5639,5640],{"class":139,"line":140},[137,5641,5642],{},"Changed value from undefined to Aleks\n",[137,5644,5645],{"class":139,"line":173},[137,5646,5647],{},"Changed value from Aleks to Nicole\n",[137,5649,5650],{"class":139,"line":188},[137,5651,5652],{},"Nicole\n",[27,5654,5655],{},"As seen above, we set the name to \"Nicole\" two times in the row but it was registered only one change of a value.",[104,5657,5659],{"id":5658},"create-a-reusable-function","Create a reusable function",[27,5661,5662],{},"The example above is a good starting point. However, we will need to repeat the same procedure over and over again, each time we define a watcher to a new variable. To avoid that hassle, we will create a reusable function:",[128,5664,5666],{"className":130,"code":5665,"language":132,"meta":133,"style":133},"const createWatchedProperty = (propertyName) => {\n    Object.defineProperty(this, propertyName, {\n        get() {\n            return this[`_${propertyName}`];\n        },\n        set(value) {\n            if (value === this[propertyName]) {\n                return;\n            }\n            const oldValue = this[propertyName];\n            this[`_${propertyName}`] = value;\n            console.log(`Changed value from ${oldValue} to ${value}`);\n        },\n    });\n};\n\ncreateWatchedProperty(\"name\");\nthis.name = \"Aleks\";\nthis.name = \"Nicole\";\nthis.name = \"Nicole\";\nconsole.log(this.name);\n",[22,5667,5668,5688,5702,5709,5728,5732,5743,5757,5764,5768,5782,5801,5821,5825,5829,5833,5837,5848,5860,5872,5884],{"__ignoreMap":133},[137,5669,5670,5672,5675,5677,5679,5682,5684,5686],{"class":139,"line":140},[137,5671,3077],{"class":143},[137,5673,5674],{"class":147}," createWatchedProperty",[137,5676,151],{"class":143},[137,5678,158],{"class":157},[137,5680,5681],{"class":161},"propertyName",[137,5683,219],{"class":157},[137,5685,222],{"class":143},[137,5687,256],{"class":157},[137,5689,5690,5693,5695,5697,5699],{"class":139,"line":173},[137,5691,5692],{"class":157},"    Object.",[137,5694,5384],{"class":147},[137,5696,356],{"class":157},[137,5698,24],{"class":364},[137,5700,5701],{"class":157},", propertyName, {\n",[137,5703,5704,5707],{"class":139,"line":188},[137,5705,5706],{"class":147},"        get",[137,5708,275],{"class":157},[137,5710,5711,5713,5715,5718,5721,5723,5725],{"class":139,"line":269},[137,5712,4683],{"class":143},[137,5714,365],{"class":364},[137,5716,5717],{"class":157},"[",[137,5719,5720],{"class":284},"`_${",[137,5722,5681],{"class":157},[137,5724,4706],{"class":284},[137,5726,5727],{"class":157},"];\n",[137,5729,5730],{"class":139,"line":278},[137,5731,2084],{"class":157},[137,5733,5734,5737,5739,5741],{"class":139,"line":291},[137,5735,5736],{"class":147},"        set",[137,5738,356],{"class":157},[137,5740,5414],{"class":161},[137,5742,170],{"class":157},[137,5744,5745,5748,5750,5752,5754],{"class":139,"line":297},[137,5746,5747],{"class":143},"            if",[137,5749,5499],{"class":157},[137,5751,5502],{"class":143},[137,5753,365],{"class":364},[137,5755,5756],{"class":157},"[propertyName]) {\n",[137,5758,5759,5762],{"class":139,"line":302},[137,5760,5761],{"class":143},"                return",[137,5763,3276],{"class":157},[137,5765,5766],{"class":139,"line":662},[137,5767,760],{"class":157},[137,5769,5770,5773,5775,5777,5779],{"class":139,"line":667},[137,5771,5772],{"class":143},"            const",[137,5774,5524],{"class":364},[137,5776,151],{"class":143},[137,5778,365],{"class":364},[137,5780,5781],{"class":157},"[propertyName];\n",[137,5783,5784,5786,5788,5790,5792,5794,5797,5799],{"class":139,"line":786},[137,5785,4576],{"class":364},[137,5787,5717],{"class":157},[137,5789,5720],{"class":284},[137,5791,5681],{"class":157},[137,5793,4706],{"class":284},[137,5795,5796],{"class":157},"] ",[137,5798,253],{"class":143},[137,5800,5543],{"class":157},[137,5802,5803,5805,5807,5809,5811,5813,5815,5817,5819],{"class":139,"line":798},[137,5804,1493],{"class":157},[137,5806,353],{"class":147},[137,5808,356],{"class":157},[137,5810,5554],{"class":284},[137,5812,5432],{"class":157},[137,5814,5559],{"class":284},[137,5816,5414],{"class":157},[137,5818,4706],{"class":284},[137,5820,1502],{"class":157},[137,5822,5823],{"class":139,"line":803},[137,5824,2084],{"class":157},[137,5826,5827],{"class":139,"line":931},[137,5828,2832],{"class":157},[137,5830,5831],{"class":139,"line":1568},[137,5832,191],{"class":157},[137,5834,5835],{"class":139,"line":1573},[137,5836,516],{"emptyLinePlaceholder":515},[137,5838,5839,5842,5844,5846],{"class":139,"line":1578},[137,5840,5841],{"class":147},"createWatchedProperty",[137,5843,356],{"class":157},[137,5845,5393],{"class":284},[137,5847,1502],{"class":157},[137,5849,5850,5852,5854,5856,5858],{"class":139,"line":1588},[137,5851,24],{"class":364},[137,5853,1397],{"class":157},[137,5855,253],{"class":143},[137,5857,5588],{"class":284},[137,5859,3276],{"class":157},[137,5861,5862,5864,5866,5868,5870],{"class":139,"line":1601},[137,5863,24],{"class":364},[137,5865,1397],{"class":157},[137,5867,253],{"class":143},[137,5869,5601],{"class":284},[137,5871,3276],{"class":157},[137,5873,5874,5876,5878,5880,5882],{"class":139,"line":3802},[137,5875,24],{"class":364},[137,5877,1397],{"class":157},[137,5879,253],{"class":143},[137,5881,5601],{"class":284},[137,5883,3276],{"class":157},[137,5885,5886,5888,5890,5892,5894],{"class":139,"line":3808},[137,5887,1436],{"class":157},[137,5889,353],{"class":147},[137,5891,356],{"class":157},[137,5893,24],{"class":364},[137,5895,505],{"class":157},[27,5897,5898,5899,5901],{},"Now, we can use ",[22,5900,5841],{}," function to create a watched property by passing the name of the variable.",[27,5903,5904],{},"At the moment we only output the old and the new variables in the console. It would be nice if can define a custom function where we can write a logic on value change. Let's see how we can do that:",[128,5906,5908],{"className":130,"code":5907,"language":132,"meta":133,"style":133},"const createWatchedProperty = (propertyName) => {\n    Object.defineProperty(this, propertyName, {\n        get() {\n            return this[`_${propertyName}`];\n        },\n        set(value) {\n            if (value === this[propertyName]) {\n                return;\n            }\n            const oldValue = this[propertyName];\n            this[`_${propertyName}`] = value;\n            this[\"watch\" + propertyName[0].toUpperCase() + propertyName.slice(1)](\n                (newVal = value),\n                (oldVal = oldValue)\n            );\n        },\n    });\n};\n\nthis.watchName = (newValue, oldValue) => {\n    console.log(\"New: \", newValue, \"Old: \", oldValue);\n};\n\ncreateWatchedProperty(\"name\");\nthis.name = \"Aleks\";\nthis.name = \"Nicole\";\nthis.name = \"Nicole\";\nconsole.log(this.name);\n",[22,5909,5910,5928,5940,5946,5962,5966,5976,5988,5994,5998,6010,6028,6069,6079,6089,6094,6098,6102,6106,6110,6136,6156,6160,6164,6174,6186,6198,6210],{"__ignoreMap":133},[137,5911,5912,5914,5916,5918,5920,5922,5924,5926],{"class":139,"line":140},[137,5913,3077],{"class":143},[137,5915,5674],{"class":147},[137,5917,151],{"class":143},[137,5919,158],{"class":157},[137,5921,5681],{"class":161},[137,5923,219],{"class":157},[137,5925,222],{"class":143},[137,5927,256],{"class":157},[137,5929,5930,5932,5934,5936,5938],{"class":139,"line":173},[137,5931,5692],{"class":157},[137,5933,5384],{"class":147},[137,5935,356],{"class":157},[137,5937,24],{"class":364},[137,5939,5701],{"class":157},[137,5941,5942,5944],{"class":139,"line":188},[137,5943,5706],{"class":147},[137,5945,275],{"class":157},[137,5947,5948,5950,5952,5954,5956,5958,5960],{"class":139,"line":269},[137,5949,4683],{"class":143},[137,5951,365],{"class":364},[137,5953,5717],{"class":157},[137,5955,5720],{"class":284},[137,5957,5681],{"class":157},[137,5959,4706],{"class":284},[137,5961,5727],{"class":157},[137,5963,5964],{"class":139,"line":278},[137,5965,2084],{"class":157},[137,5967,5968,5970,5972,5974],{"class":139,"line":291},[137,5969,5736],{"class":147},[137,5971,356],{"class":157},[137,5973,5414],{"class":161},[137,5975,170],{"class":157},[137,5977,5978,5980,5982,5984,5986],{"class":139,"line":297},[137,5979,5747],{"class":143},[137,5981,5499],{"class":157},[137,5983,5502],{"class":143},[137,5985,365],{"class":364},[137,5987,5756],{"class":157},[137,5989,5990,5992],{"class":139,"line":302},[137,5991,5761],{"class":143},[137,5993,3276],{"class":157},[137,5995,5996],{"class":139,"line":662},[137,5997,760],{"class":157},[137,5999,6000,6002,6004,6006,6008],{"class":139,"line":667},[137,6001,5772],{"class":143},[137,6003,5524],{"class":364},[137,6005,151],{"class":143},[137,6007,365],{"class":364},[137,6009,5781],{"class":157},[137,6011,6012,6014,6016,6018,6020,6022,6024,6026],{"class":139,"line":786},[137,6013,4576],{"class":364},[137,6015,5717],{"class":157},[137,6017,5720],{"class":284},[137,6019,5681],{"class":157},[137,6021,4706],{"class":284},[137,6023,5796],{"class":157},[137,6025,253],{"class":143},[137,6027,5543],{"class":157},[137,6029,6030,6032,6034,6037,6039,6042,6045,6048,6051,6053,6055,6058,6061,6063,6066],{"class":139,"line":798},[137,6031,4576],{"class":364},[137,6033,5717],{"class":157},[137,6035,6036],{"class":284},"\"watch\"",[137,6038,361],{"class":143},[137,6040,6041],{"class":157}," propertyName[",[137,6043,6044],{"class":364},"0",[137,6046,6047],{"class":157},"].",[137,6049,6050],{"class":147},"toUpperCase",[137,6052,3348],{"class":157},[137,6054,182],{"class":143},[137,6056,6057],{"class":157}," propertyName.",[137,6059,6060],{"class":147},"slice",[137,6062,356],{"class":157},[137,6064,6065],{"class":364},"1",[137,6067,6068],{"class":157},")](\n",[137,6070,6071,6074,6076],{"class":139,"line":803},[137,6072,6073],{"class":157},"                (newVal ",[137,6075,253],{"class":143},[137,6077,6078],{"class":157}," value),\n",[137,6080,6081,6084,6086],{"class":139,"line":931},[137,6082,6083],{"class":157},"                (oldVal ",[137,6085,253],{"class":143},[137,6087,6088],{"class":157}," oldValue)\n",[137,6090,6091],{"class":139,"line":1568},[137,6092,6093],{"class":157},"            );\n",[137,6095,6096],{"class":139,"line":1573},[137,6097,2084],{"class":157},[137,6099,6100],{"class":139,"line":1578},[137,6101,2832],{"class":157},[137,6103,6104],{"class":139,"line":1588},[137,6105,191],{"class":157},[137,6107,6108],{"class":139,"line":1601},[137,6109,516],{"emptyLinePlaceholder":515},[137,6111,6112,6114,6116,6119,6121,6123,6126,6128,6130,6132,6134],{"class":139,"line":3802},[137,6113,24],{"class":364},[137,6115,1017],{"class":157},[137,6117,6118],{"class":147},"watchName",[137,6120,151],{"class":143},[137,6122,158],{"class":157},[137,6124,6125],{"class":161},"newValue",[137,6127,164],{"class":157},[137,6129,5432],{"class":161},[137,6131,219],{"class":157},[137,6133,222],{"class":143},[137,6135,256],{"class":157},[137,6137,6138,6140,6142,6144,6147,6150,6153],{"class":139,"line":3808},[137,6139,493],{"class":157},[137,6141,353],{"class":147},[137,6143,356],{"class":157},[137,6145,6146],{"class":284},"\"New: \"",[137,6148,6149],{"class":157},", newValue, ",[137,6151,6152],{"class":284},"\"Old: \"",[137,6154,6155],{"class":157},", oldValue);\n",[137,6157,6158],{"class":139,"line":3822},[137,6159,191],{"class":157},[137,6161,6162],{"class":139,"line":3827},[137,6163,516],{"emptyLinePlaceholder":515},[137,6165,6166,6168,6170,6172],{"class":139,"line":3832},[137,6167,5841],{"class":147},[137,6169,356],{"class":157},[137,6171,5393],{"class":284},[137,6173,1502],{"class":157},[137,6175,6176,6178,6180,6182,6184],{"class":139,"line":3840},[137,6177,24],{"class":364},[137,6179,1397],{"class":157},[137,6181,253],{"class":143},[137,6183,5588],{"class":284},[137,6185,3276],{"class":157},[137,6187,6188,6190,6192,6194,6196],{"class":139,"line":3846},[137,6189,24],{"class":364},[137,6191,1397],{"class":157},[137,6193,253],{"class":143},[137,6195,5601],{"class":284},[137,6197,3276],{"class":157},[137,6199,6200,6202,6204,6206,6208],{"class":139,"line":3861},[137,6201,24],{"class":364},[137,6203,1397],{"class":157},[137,6205,253],{"class":143},[137,6207,5601],{"class":284},[137,6209,3276],{"class":157},[137,6211,6212,6214,6216,6218,6220],{"class":139,"line":3883},[137,6213,1436],{"class":157},[137,6215,353],{"class":147},[137,6217,356],{"class":157},[137,6219,24],{"class":364},[137,6221,505],{"class":157},[27,6223,5630],{},[128,6225,6227],{"className":5633,"code":6226,"language":5635,"meta":133,"style":133},"New:  Aleks Old:  undefined\nNew:  Nicole Old:  Aleks\nNicole\n",[22,6228,6229,6234,6239],{"__ignoreMap":133},[137,6230,6231],{"class":139,"line":140},[137,6232,6233],{},"New:  Aleks Old:  undefined\n",[137,6235,6236],{"class":139,"line":173},[137,6237,6238],{},"New:  Nicole Old:  Aleks\n",[137,6240,6241],{"class":139,"line":188},[137,6242,5652],{},[27,6244,6245,6246,6248,6249,114,6251,6253],{},"The function where we define the logic of the value change in the example above name starts with \"watch\" + the name of the variable in camel case. In our example, this will be ",[22,6247,6118],{},". The function takes two parameters ",[22,6250,6125],{},[22,6252,5432],{},". See example above.",[104,6255,6257],{"id":6256},"problem-with-the-current-solution","Problem with the current solution",[27,6259,6260],{},"The code above works perfectly fine but only when is defined in the global scope. But what if we want to defined it in an object's method?",[27,6262,6263],{},"Let's see the following example:",[128,6265,6267],{"className":130,"code":6266,"language":132,"meta":133,"style":133},"const createWatchedProperty = (propertyName) => {\n    Object.defineProperty(this, propertyName, {\n        get() {\n            return this[`_${propertyName}`];\n        },\n        set(value) {\n            if (value === this[propertyName]) {\n                return;\n            }\n            const oldValue = this[propertyName];\n            this[`_${propertyName}`] = value;\n            this[\"watch\" + propertyName[0].toUpperCase() + propertyName.slice(1)](\n                (newVal = value),\n                (oldVal = oldValue)\n            );\n        },\n    });\n};\n\nconst someObject = {\n    someFunction: function () {\n        this.watchName = (newValue, oldValue) => {\n            console.log(\"New: \", newValue, \"Old: \", oldValue);\n        };\n\n        createWatchedProperty(\"name\");\n        this.name = \"Aleks\";\n        this.name = \"Nicole\";\n        this.name = \"Nicole\";\n        console.log(this.name);\n    },\n};\n\nsomeObject.someFunction();\n",[22,6268,6269,6287,6299,6305,6321,6325,6335,6347,6353,6357,6369,6387,6419,6427,6435,6439,6443,6447,6451,6455,6466,6478,6502,6518,6522,6526,6537,6549,6561,6573,6585,6589,6593,6597],{"__ignoreMap":133},[137,6270,6271,6273,6275,6277,6279,6281,6283,6285],{"class":139,"line":140},[137,6272,3077],{"class":143},[137,6274,5674],{"class":147},[137,6276,151],{"class":143},[137,6278,158],{"class":157},[137,6280,5681],{"class":161},[137,6282,219],{"class":157},[137,6284,222],{"class":143},[137,6286,256],{"class":157},[137,6288,6289,6291,6293,6295,6297],{"class":139,"line":173},[137,6290,5692],{"class":157},[137,6292,5384],{"class":147},[137,6294,356],{"class":157},[137,6296,24],{"class":364},[137,6298,5701],{"class":157},[137,6300,6301,6303],{"class":139,"line":188},[137,6302,5706],{"class":147},[137,6304,275],{"class":157},[137,6306,6307,6309,6311,6313,6315,6317,6319],{"class":139,"line":269},[137,6308,4683],{"class":143},[137,6310,365],{"class":364},[137,6312,5717],{"class":157},[137,6314,5720],{"class":284},[137,6316,5681],{"class":157},[137,6318,4706],{"class":284},[137,6320,5727],{"class":157},[137,6322,6323],{"class":139,"line":278},[137,6324,2084],{"class":157},[137,6326,6327,6329,6331,6333],{"class":139,"line":291},[137,6328,5736],{"class":147},[137,6330,356],{"class":157},[137,6332,5414],{"class":161},[137,6334,170],{"class":157},[137,6336,6337,6339,6341,6343,6345],{"class":139,"line":297},[137,6338,5747],{"class":143},[137,6340,5499],{"class":157},[137,6342,5502],{"class":143},[137,6344,365],{"class":364},[137,6346,5756],{"class":157},[137,6348,6349,6351],{"class":139,"line":302},[137,6350,5761],{"class":143},[137,6352,3276],{"class":157},[137,6354,6355],{"class":139,"line":662},[137,6356,760],{"class":157},[137,6358,6359,6361,6363,6365,6367],{"class":139,"line":667},[137,6360,5772],{"class":143},[137,6362,5524],{"class":364},[137,6364,151],{"class":143},[137,6366,365],{"class":364},[137,6368,5781],{"class":157},[137,6370,6371,6373,6375,6377,6379,6381,6383,6385],{"class":139,"line":786},[137,6372,4576],{"class":364},[137,6374,5717],{"class":157},[137,6376,5720],{"class":284},[137,6378,5681],{"class":157},[137,6380,4706],{"class":284},[137,6382,5796],{"class":157},[137,6384,253],{"class":143},[137,6386,5543],{"class":157},[137,6388,6389,6391,6393,6395,6397,6399,6401,6403,6405,6407,6409,6411,6413,6415,6417],{"class":139,"line":798},[137,6390,4576],{"class":364},[137,6392,5717],{"class":157},[137,6394,6036],{"class":284},[137,6396,361],{"class":143},[137,6398,6041],{"class":157},[137,6400,6044],{"class":364},[137,6402,6047],{"class":157},[137,6404,6050],{"class":147},[137,6406,3348],{"class":157},[137,6408,182],{"class":143},[137,6410,6057],{"class":157},[137,6412,6060],{"class":147},[137,6414,356],{"class":157},[137,6416,6065],{"class":364},[137,6418,6068],{"class":157},[137,6420,6421,6423,6425],{"class":139,"line":803},[137,6422,6073],{"class":157},[137,6424,253],{"class":143},[137,6426,6078],{"class":157},[137,6428,6429,6431,6433],{"class":139,"line":931},[137,6430,6083],{"class":157},[137,6432,253],{"class":143},[137,6434,6088],{"class":157},[137,6436,6437],{"class":139,"line":1568},[137,6438,6093],{"class":157},[137,6440,6441],{"class":139,"line":1573},[137,6442,2084],{"class":157},[137,6444,6445],{"class":139,"line":1578},[137,6446,2832],{"class":157},[137,6448,6449],{"class":139,"line":1588},[137,6450,191],{"class":157},[137,6452,6453],{"class":139,"line":1601},[137,6454,516],{"emptyLinePlaceholder":515},[137,6456,6457,6459,6462,6464],{"class":139,"line":3802},[137,6458,3077],{"class":143},[137,6460,6461],{"class":364}," someObject",[137,6463,151],{"class":143},[137,6465,256],{"class":157},[137,6467,6468,6471,6473,6475],{"class":139,"line":3808},[137,6469,6470],{"class":147},"    someFunction",[137,6472,726],{"class":157},[137,6474,483],{"class":143},[137,6476,6477],{"class":157}," () {\n",[137,6479,6480,6482,6484,6486,6488,6490,6492,6494,6496,6498,6500],{"class":139,"line":3822},[137,6481,3679],{"class":364},[137,6483,1017],{"class":157},[137,6485,6118],{"class":147},[137,6487,151],{"class":143},[137,6489,158],{"class":157},[137,6491,6125],{"class":161},[137,6493,164],{"class":157},[137,6495,5432],{"class":161},[137,6497,219],{"class":157},[137,6499,222],{"class":143},[137,6501,256],{"class":157},[137,6503,6504,6506,6508,6510,6512,6514,6516],{"class":139,"line":3827},[137,6505,1493],{"class":157},[137,6507,353],{"class":147},[137,6509,356],{"class":157},[137,6511,6146],{"class":284},[137,6513,6149],{"class":157},[137,6515,6152],{"class":284},[137,6517,6155],{"class":157},[137,6519,6520],{"class":139,"line":3832},[137,6521,1507],{"class":157},[137,6523,6524],{"class":139,"line":3840},[137,6525,516],{"emptyLinePlaceholder":515},[137,6527,6528,6531,6533,6535],{"class":139,"line":3846},[137,6529,6530],{"class":147},"        createWatchedProperty",[137,6532,356],{"class":157},[137,6534,5393],{"class":284},[137,6536,1502],{"class":157},[137,6538,6539,6541,6543,6545,6547],{"class":139,"line":3861},[137,6540,3679],{"class":364},[137,6542,1397],{"class":157},[137,6544,253],{"class":143},[137,6546,5588],{"class":284},[137,6548,3276],{"class":157},[137,6550,6551,6553,6555,6557,6559],{"class":139,"line":3883},[137,6552,3679],{"class":364},[137,6554,1397],{"class":157},[137,6556,253],{"class":143},[137,6558,5601],{"class":284},[137,6560,3276],{"class":157},[137,6562,6563,6565,6567,6569,6571],{"class":139,"line":3896},[137,6564,3679],{"class":364},[137,6566,1397],{"class":157},[137,6568,253],{"class":143},[137,6570,5601],{"class":284},[137,6572,3276],{"class":157},[137,6574,6575,6577,6579,6581,6583],{"class":139,"line":3901},[137,6576,350],{"class":157},[137,6578,353],{"class":147},[137,6580,356],{"class":157},[137,6582,24],{"class":364},[137,6584,505],{"class":157},[137,6586,6587],{"class":139,"line":3906},[137,6588,775],{"class":157},[137,6590,6591],{"class":139,"line":3911},[137,6592,191],{"class":157},[137,6594,6595],{"class":139,"line":4666},[137,6596,516],{"emptyLinePlaceholder":515},[137,6598,6599,6602,6605],{"class":139,"line":4672},[137,6600,6601],{"class":157},"someObject.",[137,6603,6604],{"class":147},"someFunction",[137,6606,924],{"class":157},[27,6608,5630],{},[128,6610,6611],{"className":5633,"code":5652,"language":5635,"meta":133,"style":133},[22,6612,6613],{"__ignoreMap":133},[137,6614,6615],{"class":139,"line":140},[137,6616,5652],{},[27,6618,6619,6620,6622,6623,6625,6626,6628,6629,6631],{},"When we use ",[22,6621,5841],{}," function inside the object's method, ",[22,6624,24],{}," doesn't refer to the global object (window). Instead, ",[22,6627,24],{}," refers to its internal function. To solve this issue, we need to make some changes in our ",[22,6630,5841],{}," function where we can pass the object as a parameter to specify where we would like the property to be created.",[27,6633,6634],{},"Let's see the code bellow:",[128,6636,6638],{"className":130,"code":6637,"language":132,"meta":133,"style":133},"const createWatchedProperty = (propertyName, obj) => {\n    Object.defineProperty(obj, propertyName, {\n        get() {\n            return obj[`_${propertyName}`];\n        },\n        set(value) {\n            if (value === obj[propertyName]) {\n                return;\n            }\n            const oldValue = obj[propertyName];\n            obj[`_${propertyName}`] = value;\n            obj[\"watch\" + propertyName[0].toUpperCase() + propertyName.slice(1)]((newVal = value), (oldVal = oldValue));\n        },\n    });\n};\n\nconst someObject = {\n    someFunction: function () {\n        this.watchName = (newValue, oldValue) => {\n            console.log(\"New: \", newValue, \"Old: \", oldValue);\n        };\n\n        createWatchedProperty(\"name\", this);\n        this.name = \"Aleks\";\n        this.name = \"Nicole\";\n        this.name = \"Nicole\";\n        console.log(this.name);\n    },\n};\n\nsomeObject.someFunction();\n",[22,6639,6640,6662,6671,6677,6692,6696,6706,6717,6723,6727,6738,6755,6796,6800,6804,6808,6812,6822,6832,6856,6872,6876,6880,6894,6906,6918,6930,6942,6946,6950,6954],{"__ignoreMap":133},[137,6641,6642,6644,6646,6648,6650,6652,6654,6656,6658,6660],{"class":139,"line":140},[137,6643,3077],{"class":143},[137,6645,5674],{"class":147},[137,6647,151],{"class":143},[137,6649,158],{"class":157},[137,6651,5681],{"class":161},[137,6653,164],{"class":157},[137,6655,990],{"class":161},[137,6657,219],{"class":157},[137,6659,222],{"class":143},[137,6661,256],{"class":157},[137,6663,6664,6666,6668],{"class":139,"line":173},[137,6665,5692],{"class":157},[137,6667,5384],{"class":147},[137,6669,6670],{"class":157},"(obj, propertyName, {\n",[137,6672,6673,6675],{"class":139,"line":188},[137,6674,5706],{"class":147},[137,6676,275],{"class":157},[137,6678,6679,6681,6684,6686,6688,6690],{"class":139,"line":269},[137,6680,4683],{"class":143},[137,6682,6683],{"class":157}," obj[",[137,6685,5720],{"class":284},[137,6687,5681],{"class":157},[137,6689,4706],{"class":284},[137,6691,5727],{"class":157},[137,6693,6694],{"class":139,"line":278},[137,6695,2084],{"class":157},[137,6697,6698,6700,6702,6704],{"class":139,"line":291},[137,6699,5736],{"class":147},[137,6701,356],{"class":157},[137,6703,5414],{"class":161},[137,6705,170],{"class":157},[137,6707,6708,6710,6712,6714],{"class":139,"line":297},[137,6709,5747],{"class":143},[137,6711,5499],{"class":157},[137,6713,5502],{"class":143},[137,6715,6716],{"class":157}," obj[propertyName]) {\n",[137,6718,6719,6721],{"class":139,"line":302},[137,6720,5761],{"class":143},[137,6722,3276],{"class":157},[137,6724,6725],{"class":139,"line":662},[137,6726,760],{"class":157},[137,6728,6729,6731,6733,6735],{"class":139,"line":667},[137,6730,5772],{"class":143},[137,6732,5524],{"class":364},[137,6734,151],{"class":143},[137,6736,6737],{"class":157}," obj[propertyName];\n",[137,6739,6740,6743,6745,6747,6749,6751,6753],{"class":139,"line":786},[137,6741,6742],{"class":157},"            obj[",[137,6744,5720],{"class":284},[137,6746,5681],{"class":157},[137,6748,4706],{"class":284},[137,6750,5796],{"class":157},[137,6752,253],{"class":143},[137,6754,5543],{"class":157},[137,6756,6757,6759,6761,6763,6765,6767,6769,6771,6773,6775,6777,6779,6781,6783,6786,6788,6791,6793],{"class":139,"line":798},[137,6758,6742],{"class":157},[137,6760,6036],{"class":284},[137,6762,361],{"class":143},[137,6764,6041],{"class":157},[137,6766,6044],{"class":364},[137,6768,6047],{"class":157},[137,6770,6050],{"class":147},[137,6772,3348],{"class":157},[137,6774,182],{"class":143},[137,6776,6057],{"class":157},[137,6778,6060],{"class":147},[137,6780,356],{"class":157},[137,6782,6065],{"class":364},[137,6784,6785],{"class":157},")]((newVal ",[137,6787,253],{"class":143},[137,6789,6790],{"class":157}," value), (oldVal ",[137,6792,253],{"class":143},[137,6794,6795],{"class":157}," oldValue));\n",[137,6797,6798],{"class":139,"line":803},[137,6799,2084],{"class":157},[137,6801,6802],{"class":139,"line":931},[137,6803,2832],{"class":157},[137,6805,6806],{"class":139,"line":1568},[137,6807,191],{"class":157},[137,6809,6810],{"class":139,"line":1573},[137,6811,516],{"emptyLinePlaceholder":515},[137,6813,6814,6816,6818,6820],{"class":139,"line":1578},[137,6815,3077],{"class":143},[137,6817,6461],{"class":364},[137,6819,151],{"class":143},[137,6821,256],{"class":157},[137,6823,6824,6826,6828,6830],{"class":139,"line":1588},[137,6825,6470],{"class":147},[137,6827,726],{"class":157},[137,6829,483],{"class":143},[137,6831,6477],{"class":157},[137,6833,6834,6836,6838,6840,6842,6844,6846,6848,6850,6852,6854],{"class":139,"line":1601},[137,6835,3679],{"class":364},[137,6837,1017],{"class":157},[137,6839,6118],{"class":147},[137,6841,151],{"class":143},[137,6843,158],{"class":157},[137,6845,6125],{"class":161},[137,6847,164],{"class":157},[137,6849,5432],{"class":161},[137,6851,219],{"class":157},[137,6853,222],{"class":143},[137,6855,256],{"class":157},[137,6857,6858,6860,6862,6864,6866,6868,6870],{"class":139,"line":3802},[137,6859,1493],{"class":157},[137,6861,353],{"class":147},[137,6863,356],{"class":157},[137,6865,6146],{"class":284},[137,6867,6149],{"class":157},[137,6869,6152],{"class":284},[137,6871,6155],{"class":157},[137,6873,6874],{"class":139,"line":3808},[137,6875,1507],{"class":157},[137,6877,6878],{"class":139,"line":3822},[137,6879,516],{"emptyLinePlaceholder":515},[137,6881,6882,6884,6886,6888,6890,6892],{"class":139,"line":3827},[137,6883,6530],{"class":147},[137,6885,356],{"class":157},[137,6887,5393],{"class":284},[137,6889,164],{"class":157},[137,6891,24],{"class":364},[137,6893,1502],{"class":157},[137,6895,6896,6898,6900,6902,6904],{"class":139,"line":3832},[137,6897,3679],{"class":364},[137,6899,1397],{"class":157},[137,6901,253],{"class":143},[137,6903,5588],{"class":284},[137,6905,3276],{"class":157},[137,6907,6908,6910,6912,6914,6916],{"class":139,"line":3840},[137,6909,3679],{"class":364},[137,6911,1397],{"class":157},[137,6913,253],{"class":143},[137,6915,5601],{"class":284},[137,6917,3276],{"class":157},[137,6919,6920,6922,6924,6926,6928],{"class":139,"line":3846},[137,6921,3679],{"class":364},[137,6923,1397],{"class":157},[137,6925,253],{"class":143},[137,6927,5601],{"class":284},[137,6929,3276],{"class":157},[137,6931,6932,6934,6936,6938,6940],{"class":139,"line":3861},[137,6933,350],{"class":157},[137,6935,353],{"class":147},[137,6937,356],{"class":157},[137,6939,24],{"class":364},[137,6941,505],{"class":157},[137,6943,6944],{"class":139,"line":3883},[137,6945,775],{"class":157},[137,6947,6948],{"class":139,"line":3896},[137,6949,191],{"class":157},[137,6951,6952],{"class":139,"line":3901},[137,6953,516],{"emptyLinePlaceholder":515},[137,6955,6956,6958,6960],{"class":139,"line":3906},[137,6957,6601],{"class":157},[137,6959,6604],{"class":147},[137,6961,924],{"class":157},[104,6963,6965],{"id":6964},"create-subscribe-and-unsubscribe-functions","Create subscribe and unsubscribe functions",[27,6967,6968,6969,114,6972,6975,6976,6979,6980,6983,6984,6986],{},"Lastly, we can go further by creating ",[42,6970,6971],{},"subscribe",[42,6973,6974],{},"unsubscribe"," functions. In the descriptor we should set ",[22,6977,6978],{},"configurable: true",". The ",[22,6981,6982],{},"Object.defineProperty()"," defaults to non-configurable properties and we should pass it ",[22,6985,3097],{}," to allow it. This is important because we are unable to redefine the property when we unsubscribe from our watcher.",[128,6988,6990],{"className":130,"code":6989,"language":132,"meta":133,"style":133},"const subscribe = (propertyName, obj) => {\n    Object.defineProperty(obj, propertyName, {\n        configurable: true,\n        get() {\n            return obj[`_${propertyName}`];\n        },\n        set(value) {\n            if (value === obj[propertyName]) {\n                return;\n            }\n            const oldValue = obj[propertyName];\n            obj[`_${propertyName}`] = value;\n            obj[\"watch\" + propertyName[0].toUpperCase() + propertyName.slice(1)]((newVal = value), (oldVal = oldValue));\n        },\n    });\n};\n\nconst unsubscribe = (propertyName, obj) => {\n    Object.defineProperty(obj, propertyName, {\n        get() {},\n        set() {},\n    });\n};\n\nthis.watchName = (newValue, oldValue) => {\n    console.log(\"New: \", newValue, \"Old: \", oldValue);\n};\n\nsubscribe(\"name\", this);\nthis.name = \"Aleks\";\nthis.name = \"Nicole\";\nthis.name = \"Nicole\";\nthis.name = \"April\";\nunsubscribe(\"name\", this);\nthis.name = \"Tom\";\nthis.name = \"Mark\";\nthis.name = \"Bob\";\nsubscribe(\"name\", this);\nthis.name = \"Chris\";\n",[22,6991,6992,7015,7023,7032,7038,7052,7056,7066,7076,7082,7086,7096,7112,7150,7154,7158,7162,7166,7189,7197,7203,7209,7213,7217,7221,7245,7261,7265,7269,7283,7295,7307,7319,7332,7346,7359,7372,7385,7399],{"__ignoreMap":133},[137,6993,6994,6996,6999,7001,7003,7005,7007,7009,7011,7013],{"class":139,"line":140},[137,6995,3077],{"class":143},[137,6997,6998],{"class":147}," subscribe",[137,7000,151],{"class":143},[137,7002,158],{"class":157},[137,7004,5681],{"class":161},[137,7006,164],{"class":157},[137,7008,990],{"class":161},[137,7010,219],{"class":157},[137,7012,222],{"class":143},[137,7014,256],{"class":157},[137,7016,7017,7019,7021],{"class":139,"line":173},[137,7018,5692],{"class":157},[137,7020,5384],{"class":147},[137,7022,6670],{"class":157},[137,7024,7025,7028,7030],{"class":139,"line":188},[137,7026,7027],{"class":157},"        configurable: ",[137,7029,3097],{"class":364},[137,7031,1961],{"class":157},[137,7033,7034,7036],{"class":139,"line":269},[137,7035,5706],{"class":147},[137,7037,275],{"class":157},[137,7039,7040,7042,7044,7046,7048,7050],{"class":139,"line":278},[137,7041,4683],{"class":143},[137,7043,6683],{"class":157},[137,7045,5720],{"class":284},[137,7047,5681],{"class":157},[137,7049,4706],{"class":284},[137,7051,5727],{"class":157},[137,7053,7054],{"class":139,"line":291},[137,7055,2084],{"class":157},[137,7057,7058,7060,7062,7064],{"class":139,"line":297},[137,7059,5736],{"class":147},[137,7061,356],{"class":157},[137,7063,5414],{"class":161},[137,7065,170],{"class":157},[137,7067,7068,7070,7072,7074],{"class":139,"line":302},[137,7069,5747],{"class":143},[137,7071,5499],{"class":157},[137,7073,5502],{"class":143},[137,7075,6716],{"class":157},[137,7077,7078,7080],{"class":139,"line":662},[137,7079,5761],{"class":143},[137,7081,3276],{"class":157},[137,7083,7084],{"class":139,"line":667},[137,7085,760],{"class":157},[137,7087,7088,7090,7092,7094],{"class":139,"line":786},[137,7089,5772],{"class":143},[137,7091,5524],{"class":364},[137,7093,151],{"class":143},[137,7095,6737],{"class":157},[137,7097,7098,7100,7102,7104,7106,7108,7110],{"class":139,"line":798},[137,7099,6742],{"class":157},[137,7101,5720],{"class":284},[137,7103,5681],{"class":157},[137,7105,4706],{"class":284},[137,7107,5796],{"class":157},[137,7109,253],{"class":143},[137,7111,5543],{"class":157},[137,7113,7114,7116,7118,7120,7122,7124,7126,7128,7130,7132,7134,7136,7138,7140,7142,7144,7146,7148],{"class":139,"line":803},[137,7115,6742],{"class":157},[137,7117,6036],{"class":284},[137,7119,361],{"class":143},[137,7121,6041],{"class":157},[137,7123,6044],{"class":364},[137,7125,6047],{"class":157},[137,7127,6050],{"class":147},[137,7129,3348],{"class":157},[137,7131,182],{"class":143},[137,7133,6057],{"class":157},[137,7135,6060],{"class":147},[137,7137,356],{"class":157},[137,7139,6065],{"class":364},[137,7141,6785],{"class":157},[137,7143,253],{"class":143},[137,7145,6790],{"class":157},[137,7147,253],{"class":143},[137,7149,6795],{"class":157},[137,7151,7152],{"class":139,"line":931},[137,7153,2084],{"class":157},[137,7155,7156],{"class":139,"line":1568},[137,7157,2832],{"class":157},[137,7159,7160],{"class":139,"line":1573},[137,7161,191],{"class":157},[137,7163,7164],{"class":139,"line":1578},[137,7165,516],{"emptyLinePlaceholder":515},[137,7167,7168,7170,7173,7175,7177,7179,7181,7183,7185,7187],{"class":139,"line":1588},[137,7169,3077],{"class":143},[137,7171,7172],{"class":147}," unsubscribe",[137,7174,151],{"class":143},[137,7176,158],{"class":157},[137,7178,5681],{"class":161},[137,7180,164],{"class":157},[137,7182,990],{"class":161},[137,7184,219],{"class":157},[137,7186,222],{"class":143},[137,7188,256],{"class":157},[137,7190,7191,7193,7195],{"class":139,"line":1601},[137,7192,5692],{"class":157},[137,7194,5384],{"class":147},[137,7196,6670],{"class":157},[137,7198,7199,7201],{"class":139,"line":3802},[137,7200,5706],{"class":147},[137,7202,5404],{"class":157},[137,7204,7205,7207],{"class":139,"line":3808},[137,7206,5736],{"class":147},[137,7208,5404],{"class":157},[137,7210,7211],{"class":139,"line":3822},[137,7212,2832],{"class":157},[137,7214,7215],{"class":139,"line":3827},[137,7216,191],{"class":157},[137,7218,7219],{"class":139,"line":3832},[137,7220,516],{"emptyLinePlaceholder":515},[137,7222,7223,7225,7227,7229,7231,7233,7235,7237,7239,7241,7243],{"class":139,"line":3840},[137,7224,24],{"class":364},[137,7226,1017],{"class":157},[137,7228,6118],{"class":147},[137,7230,151],{"class":143},[137,7232,158],{"class":157},[137,7234,6125],{"class":161},[137,7236,164],{"class":157},[137,7238,5432],{"class":161},[137,7240,219],{"class":157},[137,7242,222],{"class":143},[137,7244,256],{"class":157},[137,7246,7247,7249,7251,7253,7255,7257,7259],{"class":139,"line":3846},[137,7248,493],{"class":157},[137,7250,353],{"class":147},[137,7252,356],{"class":157},[137,7254,6146],{"class":284},[137,7256,6149],{"class":157},[137,7258,6152],{"class":284},[137,7260,6155],{"class":157},[137,7262,7263],{"class":139,"line":3861},[137,7264,191],{"class":157},[137,7266,7267],{"class":139,"line":3883},[137,7268,516],{"emptyLinePlaceholder":515},[137,7270,7271,7273,7275,7277,7279,7281],{"class":139,"line":3896},[137,7272,6971],{"class":147},[137,7274,356],{"class":157},[137,7276,5393],{"class":284},[137,7278,164],{"class":157},[137,7280,24],{"class":364},[137,7282,1502],{"class":157},[137,7284,7285,7287,7289,7291,7293],{"class":139,"line":3901},[137,7286,24],{"class":364},[137,7288,1397],{"class":157},[137,7290,253],{"class":143},[137,7292,5588],{"class":284},[137,7294,3276],{"class":157},[137,7296,7297,7299,7301,7303,7305],{"class":139,"line":3906},[137,7298,24],{"class":364},[137,7300,1397],{"class":157},[137,7302,253],{"class":143},[137,7304,5601],{"class":284},[137,7306,3276],{"class":157},[137,7308,7309,7311,7313,7315,7317],{"class":139,"line":3911},[137,7310,24],{"class":364},[137,7312,1397],{"class":157},[137,7314,253],{"class":143},[137,7316,5601],{"class":284},[137,7318,3276],{"class":157},[137,7320,7321,7323,7325,7327,7330],{"class":139,"line":4666},[137,7322,24],{"class":364},[137,7324,1397],{"class":157},[137,7326,253],{"class":143},[137,7328,7329],{"class":284}," \"April\"",[137,7331,3276],{"class":157},[137,7333,7334,7336,7338,7340,7342,7344],{"class":139,"line":4672},[137,7335,6974],{"class":147},[137,7337,356],{"class":157},[137,7339,5393],{"class":284},[137,7341,164],{"class":157},[137,7343,24],{"class":364},[137,7345,1502],{"class":157},[137,7347,7348,7350,7352,7354,7357],{"class":139,"line":4680},[137,7349,24],{"class":364},[137,7351,1397],{"class":157},[137,7353,253],{"class":143},[137,7355,7356],{"class":284}," \"Tom\"",[137,7358,3276],{"class":157},[137,7360,7361,7363,7365,7367,7370],{"class":139,"line":4711},[137,7362,24],{"class":364},[137,7364,1397],{"class":157},[137,7366,253],{"class":143},[137,7368,7369],{"class":284}," \"Mark\"",[137,7371,3276],{"class":157},[137,7373,7374,7376,7378,7380,7383],{"class":139,"line":4716},[137,7375,24],{"class":364},[137,7377,1397],{"class":157},[137,7379,253],{"class":143},[137,7381,7382],{"class":284}," \"Bob\"",[137,7384,3276],{"class":157},[137,7386,7387,7389,7391,7393,7395,7397],{"class":139,"line":4721},[137,7388,6971],{"class":147},[137,7390,356],{"class":157},[137,7392,5393],{"class":284},[137,7394,164],{"class":157},[137,7396,24],{"class":364},[137,7398,1502],{"class":157},[137,7400,7401,7403,7405,7407,7410],{"class":139,"line":4727},[137,7402,24],{"class":364},[137,7404,1397],{"class":157},[137,7406,253],{"class":143},[137,7408,7409],{"class":284}," \"Chris\"",[137,7411,3276],{"class":157},[27,7413,5630],{},[128,7415,7417],{"className":130,"code":7416,"language":132,"meta":133,"style":133},"New:  Aleks Old:  undefined\nNew:  Nicole Old:  Aleks\nNew:  April Old:  Nicole\nNew:  Chris Old:  April\n",[22,7418,7419,7436,7448,7460],{"__ignoreMap":133},[137,7420,7421,7424,7427,7430,7433],{"class":139,"line":140},[137,7422,7423],{"class":147},"New",[137,7425,7426],{"class":157},":  Aleks ",[137,7428,7429],{"class":147},"Old",[137,7431,7432],{"class":157},":  ",[137,7434,7435],{"class":364},"undefined\n",[137,7437,7438,7440,7443,7445],{"class":139,"line":173},[137,7439,7423],{"class":147},[137,7441,7442],{"class":157},":  Nicole ",[137,7444,7429],{"class":147},[137,7446,7447],{"class":157},":  Aleks\n",[137,7449,7450,7452,7455,7457],{"class":139,"line":188},[137,7451,7423],{"class":147},[137,7453,7454],{"class":157},":  April ",[137,7456,7429],{"class":147},[137,7458,7459],{"class":157},":  Nicole\n",[137,7461,7462,7464,7467,7469],{"class":139,"line":269},[137,7463,7423],{"class":147},[137,7465,7466],{"class":157},":  Chris ",[137,7468,7429],{"class":147},[137,7470,7471],{"class":157},":  April\n",[104,7473,2567],{"id":2566},[2569,7475,7476,7479,7485,7488],{},[1006,7477,7478],{},"In JavaScript there isn't an event that fires when a value of a variable changes. But that is completely possible to be implemented by defining getters and setters in the object.",[1006,7480,7481,7482,7484],{},"We use ",[22,7483,5359],{}," method to define a variable that we can watch for changes.",[1006,7486,7487],{},"It is a good practice to create a reusable function for creating a watched property, where we can pass an object argument in order for the variable to be defined in that particular object.",[1006,7489,7490,7491,114,7493,7495,7496,7498],{},"Creating ",[42,7492,6971],{},[42,7494,6974],{}," functions allow us to add and remove a watcher to the variable. That way, we can stop watching a variable change when we need to. Remember to add set ",[22,7497,6978],{}," in the descriptor. This will allow us to redefined the property when we unsubscribe.",[27,7500,5276,7501,1017],{},[45,7502,2726],{"href":7503,"target":2716,"rel":7504},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fwatch-javascript-variables",[2718,2719],[2617,7506,7507],{},"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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":133,"searchDepth":173,"depth":173,"links":7509},[7510,7511,7512,7513],{"id":5658,"depth":173,"text":5659},{"id":6256,"depth":173,"text":6257},{"id":6964,"depth":173,"text":6965},{"id":2566,"depth":173,"text":2567},"We all know that in JavaScript there is no event that fires when a value of a variable changes. But by defining getters and setters in the object this is now possible. First, define a new property on an object with \"Object.defineProperty\" method. As a first argument, we pass the object on which to define the property. If the object is defined in the global scope, \"this\" will refer to the window object. In the second argument, we will define the name of the property. In our case, we define a property with name \"name\". The third argument is the descriptor for the property being defined.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1630971440\u002Fblog\u002Fwatch-javascript-variables-for-change",[7517,7518,7519,7520,5359,5299,5300],"Watch JavaScript Variables for change","Listen to variables","JavaScript Object","Getters and Setters",{},"\u002F2021\u002F09\u002F07\u002Fwatch-javascript-variables-for-change","7th Sep 2021",{"title":5309,"description":7514},"2021\u002F09\u002F07\u002Fwatch-javascript-variables-for-change","9FZMyOqW-5nGAWv9Df3JG1sqdptna2IOoYAqSXPP0aU",{"id":7528,"title":7529,"articleTags":7530,"author":11,"blog":12,"body":7532,"description":8445,"extension":2649,"image":8446,"keywords":8447,"meta":8451,"navigation":515,"path":8452,"published":8453,"readTime":188,"seo":8454,"stem":8455,"type":2662,"__hash__":8456},"content\u002F2021\u002F09\u002F14\u002Fstyle-binding-in-sfc-in-vue-3.md","Style Binding in SFC in Vue 3",[8,9,7531],"CSS",{"type":14,"value":7533,"toc":8440},[7534,7537,7552,7554,7558,7563,7577,7581,7590,7623,7630,7708,7714,7748,7757,7800,7810,7853,7856,7860,7866,8300,8303,8308,8310,8319,8428,8431,8437],[17,7535,7529],{"id":7536},"style-binding-in-sfc-in-vue-3",[27,7538,7539],{},[30,7540,7541,36,7543,40,7545],{},[33,7542],{"value":35},[33,7544],{"value":39},[42,7546,7547],{},[45,7548,7550],{"href":47,"target":2716,"rel":7549},[2718,2719],[33,7551],{"value":50},[52,7553],{":tags":54},[56,7555],{":audio-src":7556,":transcript-src":7557},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F09\u002F14\u002Fstyle-binding-in-sfc-in-vue-3\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F09\u002F14\u002Fstyle-binding-in-sfc-in-vue-3\u002Fsummary.json",[27,7559,7560],{},[63,7561],{"alt":65,"src":7562},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1631578776\u002Fblog\u002Fstyle-binding-in-sfc-in-vue-3\u002Fstyle-binding-in-sfc-in-vue-3",[27,7564,7565,7566,7569,7570,7572,7573,7576],{},"For a long time, Vue as a framework had a built-in feature of binding inline styles in HTML templates. But what has changed since the new Vue 3.2 release is that we can now also use the existing ",[22,7567,7568],{},"v-bind"," syntax with reactive variables inside our ",[22,7571,3972],{}," tag in the ",[22,7574,7575],{},".vue"," files aka Single File Components (SFC). Let's take a look how can we do that:",[104,7578,7580],{"id":7579},"binding-a-random-color","Binding a Random Color",[27,7582,7583,7584,7586,7587,1017],{},"Firstly, we should declare a reactive variable in the ",[22,7585,1942],{}," function called ",[22,7588,7589],{},"randomColor",[128,7591,7593],{"className":130,"code":7592,"language":132,"meta":133,"style":133},"data() {\n    return {\n        randomColor: \"\"\n    };\n}\n",[22,7594,7595,7601,7607,7615,7619],{"__ignoreMap":133},[137,7596,7597,7599],{"class":139,"line":140},[137,7598,1942],{"class":147},[137,7600,275],{"class":157},[137,7602,7603,7605],{"class":139,"line":173},[137,7604,176],{"class":143},[137,7606,256],{"class":157},[137,7608,7609,7612],{"class":139,"line":188},[137,7610,7611],{"class":157},"        randomColor: ",[137,7613,7614],{"class":284},"\"\"\n",[137,7616,7617],{"class":139,"line":269},[137,7618,1892],{"class":157},[137,7620,7621],{"class":139,"line":278},[137,7622,510],{"class":157},[27,7624,7625,7626,7629],{},"Next, we declare a method called ",[22,7627,7628],{},"getRandomColor()"," that returns a random colour.",[128,7631,7633],{"className":130,"code":7632,"language":132,"meta":133,"style":133},"methods: {\n    getRandomColor() {\n        return \"#\" + (((1 \u003C\u003C 24) * Math.random()) | 0).toString(16);\n    }\n}\n",[22,7634,7635,7641,7648,7700,7704],{"__ignoreMap":133},[137,7636,7637,7639],{"class":139,"line":140},[137,7638,1816],{"class":147},[137,7640,1819],{"class":157},[137,7642,7643,7646],{"class":139,"line":173},[137,7644,7645],{"class":147},"    getRandomColor",[137,7647,275],{"class":157},[137,7649,7650,7652,7655,7657,7660,7662,7665,7668,7670,7673,7676,7679,7682,7685,7688,7690,7693,7695,7698],{"class":139,"line":188},[137,7651,5472],{"class":143},[137,7653,7654],{"class":284}," \"#\"",[137,7656,361],{"class":143},[137,7658,7659],{"class":157}," (((",[137,7661,6065],{"class":364},[137,7663,7664],{"class":143}," \u003C\u003C",[137,7666,7667],{"class":364}," 24",[137,7669,219],{"class":157},[137,7671,7672],{"class":143},"*",[137,7674,7675],{"class":157}," Math.",[137,7677,7678],{"class":147},"random",[137,7680,7681],{"class":157},"()) ",[137,7683,7684],{"class":143},"|",[137,7686,7687],{"class":364}," 0",[137,7689,4409],{"class":157},[137,7691,7692],{"class":147},"toString",[137,7694,356],{"class":157},[137,7696,7697],{"class":364},"16",[137,7699,1502],{"class":157},[137,7701,7702],{"class":139,"line":269},[137,7703,294],{"class":157},[137,7705,7706],{"class":139,"line":278},[137,7707,510],{"class":157},[27,7709,7710,7711,7713],{},"We assign a random colour to the ",[22,7712,7589],{}," reactive variable when the component has been mounted.",[128,7715,7717],{"className":130,"code":7716,"language":132,"meta":133,"style":133},"mounted() {\n    this.randomColor = this.getRandomColor();\n}\n",[22,7718,7719,7726,7744],{"__ignoreMap":133},[137,7720,7721,7724],{"class":139,"line":140},[137,7722,7723],{"class":147},"mounted",[137,7725,275],{"class":157},[137,7727,7728,7730,7733,7735,7737,7739,7742],{"class":139,"line":173},[137,7729,1394],{"class":364},[137,7731,7732],{"class":157},".randomColor ",[137,7734,253],{"class":143},[137,7736,365],{"class":364},[137,7738,1017],{"class":157},[137,7740,7741],{"class":147},"getRandomColor",[137,7743,924],{"class":157},[137,7745,7746],{"class":139,"line":188},[137,7747,510],{"class":157},[27,7749,7750,7751,7753,7754,7756],{},"Now we use ",[22,7752,7568],{}," syntax within our ",[22,7755,3972],{}," tag to set our colour.",[128,7758,7760],{"className":4024,"code":7759,"language":4026,"meta":133,"style":133},"\u003Cstyle scoped>\n    .text {\n        color: v-bind(randomColor);\n    }\n\u003C\u002Fstyle>\n",[22,7761,7762,7773,7780,7788,7792],{"__ignoreMap":133},[137,7763,7764,7766,7768,7771],{"class":139,"line":140},[137,7765,4033],{"class":157},[137,7767,2617],{"class":4036},[137,7769,7770],{"class":147}," scoped",[137,7772,4053],{"class":157},[137,7774,7775,7778],{"class":139,"line":173},[137,7776,7777],{"class":147},"    .text",[137,7779,256],{"class":157},[137,7781,7782,7785],{"class":139,"line":188},[137,7783,7784],{"class":364},"        color",[137,7786,7787],{"class":157},": v-bind(randomColor);\n",[137,7789,7790],{"class":139,"line":269},[137,7791,294],{"class":157},[137,7793,7794,7796,7798],{"class":139,"line":278},[137,7795,4083],{"class":157},[137,7797,2617],{"class":4036},[137,7799,4053],{"class":157},[27,7801,7802,7803,7806,7807,1017],{},"Lastly, we create ",[22,7804,7805],{},"\u003Ch1>"," element in our vue template with a class of ",[22,7808,7809],{},"class=\"text\"",[128,7811,7813],{"className":4024,"code":7812,"language":4026,"meta":133,"style":133},"\u003Ctemplate>\n    \u003Ch1 class=\"text\">Hi There!\u003C\u002Fh1>\n\u003C\u002Ftemplate>\n",[22,7814,7815,7824,7845],{"__ignoreMap":133},[137,7816,7817,7819,7822],{"class":139,"line":140},[137,7818,4033],{"class":157},[137,7820,7821],{"class":4036},"template",[137,7823,4053],{"class":157},[137,7825,7826,7828,7830,7833,7835,7838,7841,7843],{"class":139,"line":173},[137,7827,4072],{"class":157},[137,7829,17],{"class":4036},[137,7831,7832],{"class":147}," class",[137,7834,253],{"class":157},[137,7836,7837],{"class":284},"\"text\"",[137,7839,7840],{"class":157},">Hi There!\u003C\u002F",[137,7842,17],{"class":4036},[137,7844,4053],{"class":157},[137,7846,7847,7849,7851],{"class":139,"line":188},[137,7848,4083],{"class":157},[137,7850,7821],{"class":4036},[137,7852,4053],{"class":157},[27,7854,7855],{},"Now every time our component re-renders, we will have a different colour on our text.",[104,7857,7859],{"id":7858},"expand-the-example-above-with-some-more-styles","Expand the example above with some more styles.",[27,7861,7862,7863,7865],{},"In the previous example, we saw how easy was to bind reactive variables in our ",[22,7864,3972],{}," tag. Now let's add more styles to our example. We are going to create an addition button, so on click a change will be noted with the colour of the text, the size and the colour of the border, as well as the font size of the button. Let's have look on how we can do that:",[128,7867,7869],{"className":130,"code":7868,"language":132,"meta":133,"style":133},"\u003Cscript>\nexport default {\n    data() {\n        return {\n            randomColor: \"blue\",\n            borderStyle: \"2px solid red\",\n            buttonFontSize: \"2\",\n        };\n    },\n    methods: {\n        getRandomColor() {\n            return \"#\" + (((1 \u003C\u003C 24) * Math.random()) | 0).toString(16);\n        },\n        getRandomNumber() {\n            return Math.floor(Math.random() * 10) + 1;\n        },\n        pickStyle() {\n            this.randomColor = this.getRandomColor();\n            this.borderStyle =\n                this.getRandomNumber() + \"px solid \" + this.getRandomColor();\n            this.buttonFontSize = this.getRandomNumber();\n        },\n    },\n};\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Ch1 class=\"text\">Hi There!\u003C\u002Fh1>\n    \u003Cbutton class=\"btn\" @click=\"pickStyle\">Random color\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.text {\n    color: v-bind(randomColor);\n    border: v-bind(borderStyle);\n    padding: 20px;\n}\n.btn {\n    cursor: pointer;\n    font-size: calc(v-bind(buttonFontSize + \"px\") * 10);\n}\n\u003C\u002Fstyle>\n",[22,7870,7871,7879,7884,7891,7896,7908,7920,7932,7936,7940,7945,7952,7992,7996,8003,8033,8037,8044,8060,8070,8097,8114,8118,8122,8126,8134,8138,8146,8164,8185,8190,8194,8203,8208,8221,8233,8238,8242,8247,8252,8288,8292],{"__ignoreMap":133},[137,7872,7873,7875,7877],{"class":139,"line":140},[137,7874,4033],{"class":157},[137,7876,4037],{"class":4036},[137,7878,4053],{"class":157},[137,7880,7881],{"class":139,"line":173},[137,7882,7883],{"class":157},"export default {\n",[137,7885,7886,7889],{"class":139,"line":188},[137,7887,7888],{"class":147},"    data",[137,7890,275],{"class":157},[137,7892,7893],{"class":139,"line":269},[137,7894,7895],{"class":157},"        return {\n",[137,7897,7898,7901,7903,7906],{"class":139,"line":278},[137,7899,7900],{"class":147},"            randomColor",[137,7902,726],{"class":157},[137,7904,7905],{"class":284},"\"blue\"",[137,7907,1961],{"class":157},[137,7909,7910,7913,7915,7918],{"class":139,"line":291},[137,7911,7912],{"class":147},"            borderStyle",[137,7914,726],{"class":157},[137,7916,7917],{"class":284},"\"2px solid red\"",[137,7919,1961],{"class":157},[137,7921,7922,7925,7927,7930],{"class":139,"line":297},[137,7923,7924],{"class":147},"            buttonFontSize",[137,7926,726],{"class":157},[137,7928,7929],{"class":284},"\"2\"",[137,7931,1961],{"class":157},[137,7933,7934],{"class":139,"line":302},[137,7935,1507],{"class":157},[137,7937,7938],{"class":139,"line":662},[137,7939,775],{"class":157},[137,7941,7942],{"class":139,"line":667},[137,7943,7944],{"class":157},"    methods: {\n",[137,7946,7947,7950],{"class":139,"line":786},[137,7948,7949],{"class":147},"        getRandomColor",[137,7951,275],{"class":157},[137,7953,7954,7956,7958,7960,7962,7964,7966,7968,7970,7972,7974,7976,7978,7980,7982,7984,7986,7988,7990],{"class":139,"line":798},[137,7955,4683],{"class":143},[137,7957,7654],{"class":284},[137,7959,361],{"class":143},[137,7961,7659],{"class":157},[137,7963,6065],{"class":364},[137,7965,7664],{"class":143},[137,7967,7667],{"class":364},[137,7969,219],{"class":157},[137,7971,7672],{"class":143},[137,7973,7675],{"class":157},[137,7975,7678],{"class":147},[137,7977,7681],{"class":157},[137,7979,7684],{"class":143},[137,7981,7687],{"class":364},[137,7983,4409],{"class":157},[137,7985,7692],{"class":147},[137,7987,356],{"class":157},[137,7989,7697],{"class":364},[137,7991,1502],{"class":157},[137,7993,7994],{"class":139,"line":803},[137,7995,2084],{"class":157},[137,7997,7998,8001],{"class":139,"line":931},[137,7999,8000],{"class":147},"        getRandomNumber",[137,8002,275],{"class":157},[137,8004,8005,8007,8009,8012,8015,8017,8019,8021,8024,8026,8028,8031],{"class":139,"line":1568},[137,8006,4683],{"class":143},[137,8008,7675],{"class":157},[137,8010,8011],{"class":147},"floor",[137,8013,8014],{"class":157},"(Math.",[137,8016,7678],{"class":147},[137,8018,3348],{"class":157},[137,8020,7672],{"class":143},[137,8022,8023],{"class":364}," 10",[137,8025,219],{"class":157},[137,8027,182],{"class":143},[137,8029,8030],{"class":364}," 1",[137,8032,3276],{"class":157},[137,8034,8035],{"class":139,"line":1573},[137,8036,2084],{"class":157},[137,8038,8039,8042],{"class":139,"line":1578},[137,8040,8041],{"class":147},"        pickStyle",[137,8043,275],{"class":157},[137,8045,8046,8048,8050,8052,8054,8056,8058],{"class":139,"line":1588},[137,8047,4576],{"class":364},[137,8049,7732],{"class":157},[137,8051,253],{"class":143},[137,8053,365],{"class":364},[137,8055,1017],{"class":157},[137,8057,7741],{"class":147},[137,8059,924],{"class":157},[137,8061,8062,8064,8067],{"class":139,"line":1601},[137,8063,4576],{"class":364},[137,8065,8066],{"class":157},".borderStyle ",[137,8068,8069],{"class":143},"=\n",[137,8071,8072,8075,8077,8080,8082,8084,8087,8089,8091,8093,8095],{"class":139,"line":3802},[137,8073,8074],{"class":364},"                this",[137,8076,1017],{"class":157},[137,8078,8079],{"class":147},"getRandomNumber",[137,8081,3348],{"class":157},[137,8083,182],{"class":143},[137,8085,8086],{"class":284}," \"px solid \"",[137,8088,361],{"class":143},[137,8090,365],{"class":364},[137,8092,1017],{"class":157},[137,8094,7741],{"class":147},[137,8096,924],{"class":157},[137,8098,8099,8101,8104,8106,8108,8110,8112],{"class":139,"line":3808},[137,8100,4576],{"class":364},[137,8102,8103],{"class":157},".buttonFontSize ",[137,8105,253],{"class":143},[137,8107,365],{"class":364},[137,8109,1017],{"class":157},[137,8111,8079],{"class":147},[137,8113,924],{"class":157},[137,8115,8116],{"class":139,"line":3822},[137,8117,2084],{"class":157},[137,8119,8120],{"class":139,"line":3827},[137,8121,775],{"class":157},[137,8123,8124],{"class":139,"line":3832},[137,8125,191],{"class":157},[137,8127,8128,8130,8132],{"class":139,"line":3840},[137,8129,4083],{"class":157},[137,8131,4037],{"class":4036},[137,8133,4053],{"class":157},[137,8135,8136],{"class":139,"line":3846},[137,8137,516],{"emptyLinePlaceholder":515},[137,8139,8140,8142,8144],{"class":139,"line":3861},[137,8141,4033],{"class":157},[137,8143,7821],{"class":4036},[137,8145,4053],{"class":157},[137,8147,8148,8150,8152,8154,8156,8158,8160,8162],{"class":139,"line":3883},[137,8149,4072],{"class":157},[137,8151,17],{"class":4036},[137,8153,7832],{"class":147},[137,8155,253],{"class":143},[137,8157,7837],{"class":284},[137,8159,7840],{"class":157},[137,8161,17],{"class":4036},[137,8163,4053],{"class":157},[137,8165,8166,8168,8171,8173,8175,8178,8182],{"class":139,"line":3896},[137,8167,4072],{"class":157},[137,8169,8170],{"class":4036},"button",[137,8172,7832],{"class":147},[137,8174,253],{"class":143},[137,8176,8177],{"class":284},"\"btn\"",[137,8179,8181],{"class":8180},"s7hpK"," @click=\"pickStyle\">Random",[137,8183,8184],{"class":8180}," color\u003C\u002Fbutton>\n",[137,8186,8187],{"class":139,"line":3901},[137,8188,8189],{"class":8180},"\u003C\u002Ftemplate>\n",[137,8191,8192],{"class":139,"line":3906},[137,8193,516],{"emptyLinePlaceholder":515},[137,8195,8196,8199,8201],{"class":139,"line":3911},[137,8197,8198],{"class":8180},"\u003Cstyle",[137,8200,7770],{"class":147},[137,8202,4053],{"class":157},[137,8204,8205],{"class":139,"line":4666},[137,8206,8207],{"class":157},".text {\n",[137,8209,8210,8213,8216,8218],{"class":139,"line":4672},[137,8211,8212],{"class":157},"    color: v",[137,8214,8215],{"class":143},"-",[137,8217,1000],{"class":147},[137,8219,8220],{"class":157},"(randomColor);\n",[137,8222,8223,8226,8228,8230],{"class":139,"line":4680},[137,8224,8225],{"class":157},"    border: v",[137,8227,8215],{"class":143},[137,8229,1000],{"class":147},[137,8231,8232],{"class":157},"(borderStyle);\n",[137,8234,8235],{"class":139,"line":4711},[137,8236,8237],{"class":157},"    padding: 20px;\n",[137,8239,8240],{"class":139,"line":4716},[137,8241,510],{"class":157},[137,8243,8244],{"class":139,"line":4721},[137,8245,8246],{"class":157},".btn {\n",[137,8248,8249],{"class":139,"line":4727},[137,8250,8251],{"class":157},"    cursor: pointer;\n",[137,8253,8254,8257,8259,8262,8265,8268,8270,8272,8275,8277,8280,8282,8284,8286],{"class":139,"line":4732},[137,8255,8256],{"class":157},"    font",[137,8258,8215],{"class":143},[137,8260,8261],{"class":157},"size: ",[137,8263,8264],{"class":147},"calc",[137,8266,8267],{"class":157},"(v",[137,8269,8215],{"class":143},[137,8271,1000],{"class":147},[137,8273,8274],{"class":157},"(buttonFontSize ",[137,8276,182],{"class":143},[137,8278,8279],{"class":284}," \"px\"",[137,8281,219],{"class":157},[137,8283,7672],{"class":143},[137,8285,8023],{"class":364},[137,8287,1502],{"class":157},[137,8289,8290],{"class":139,"line":5006},[137,8291,510],{"class":157},[137,8293,8294,8296,8298],{"class":139,"line":5014},[137,8295,4083],{"class":157},[137,8297,2617],{"class":4036},[137,8299,4053],{"class":157},[27,8301,8302],{},"With each click of the button we get different styles. See the gif animation below:",[27,8304,8305],{},[63,8306],{"alt":65,"src":8307},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1631578836\u002Fblog\u002Fstyle-binding-in-sfc-in-vue-3\u002Fstyle-binding-random-colour",[104,8309,2567],{"id":2566},[27,8311,8312,8313,8315,8316,8318],{},"With the two examples above, we saw how easy it was to implement a style binding using ",[22,8314,7568],{}," syntax in Vue 3.2. But the question is, how has Vue allowed for this? If we inspect the elements in the DevTools of the browser, we will see that all elements used ",[22,8317,7568],{}," have their inline styles with CSS variable in them.",[128,8320,8322],{"className":4024,"code":8321,"language":4026,"meta":133,"style":133},"\u003Cdiv>\n    \u003Ch1\n        class=\"text\"\n        style=\"--0d3f91e2-randomColor:#a36835; --0d3f91e2-borderStyle:10px solid #3e58fa; --0d3f91e2-buttonFontSize____px_:5px;\"\n    >\n        Hi There!\n    \u003C\u002Fh1>\n    \u003Cbutton\n        class=\"btn\"\n        style=\"--0d3f91e2-randomColor:#a36835; --0d3f91e2-borderStyle:10px solid #3e58fa; --0d3f91e2-buttonFontSize____px_:5px;\"\n    >\n        Random color\n    \u003C\u002Fbutton>\n\u003C\u002Fdiv>\n",[22,8323,8324,8333,8340,8350,8360,8365,8370,8379,8386,8395,8403,8407,8412,8420],{"__ignoreMap":133},[137,8325,8326,8328,8331],{"class":139,"line":140},[137,8327,4033],{"class":157},[137,8329,8330],{"class":4036},"div",[137,8332,4053],{"class":157},[137,8334,8335,8337],{"class":139,"line":173},[137,8336,4072],{"class":157},[137,8338,8339],{"class":4036},"h1\n",[137,8341,8342,8345,8347],{"class":139,"line":188},[137,8343,8344],{"class":147},"        class",[137,8346,253],{"class":157},[137,8348,8349],{"class":284},"\"text\"\n",[137,8351,8352,8355,8357],{"class":139,"line":269},[137,8353,8354],{"class":147},"        style",[137,8356,253],{"class":157},[137,8358,8359],{"class":284},"\"--0d3f91e2-randomColor:#a36835; --0d3f91e2-borderStyle:10px solid #3e58fa; --0d3f91e2-buttonFontSize____px_:5px;\"\n",[137,8361,8362],{"class":139,"line":278},[137,8363,8364],{"class":157},"    >\n",[137,8366,8367],{"class":139,"line":291},[137,8368,8369],{"class":157},"        Hi There!\n",[137,8371,8372,8375,8377],{"class":139,"line":297},[137,8373,8374],{"class":157},"    \u003C\u002F",[137,8376,17],{"class":4036},[137,8378,4053],{"class":157},[137,8380,8381,8383],{"class":139,"line":302},[137,8382,4072],{"class":157},[137,8384,8385],{"class":4036},"button\n",[137,8387,8388,8390,8392],{"class":139,"line":662},[137,8389,8344],{"class":147},[137,8391,253],{"class":157},[137,8393,8394],{"class":284},"\"btn\"\n",[137,8396,8397,8399,8401],{"class":139,"line":667},[137,8398,8354],{"class":147},[137,8400,253],{"class":157},[137,8402,8359],{"class":284},[137,8404,8405],{"class":139,"line":786},[137,8406,8364],{"class":157},[137,8408,8409],{"class":139,"line":798},[137,8410,8411],{"class":157},"        Random color\n",[137,8413,8414,8416,8418],{"class":139,"line":803},[137,8415,8374],{"class":157},[137,8417,8170],{"class":4036},[137,8419,4053],{"class":157},[137,8421,8422,8424,8426],{"class":139,"line":931},[137,8423,4083],{"class":157},[137,8425,8330],{"class":4036},[137,8427,4053],{"class":157},[27,8429,8430],{},"So what Vue has done is to assign native CSS variables to each the elements in their inline styles so that Vue can update the values of the variables when needed.",[27,8432,5276,8433,1017],{},[45,8434,2726],{"href":8435,"target":2716,"rel":8436},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fstyle-binding-in-single-file-component-in-vue-3",[2718,2719],[2617,8438,8439],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":133,"searchDepth":173,"depth":173,"links":8441},[8442,8443,8444],{"id":7579,"depth":173,"text":7580},{"id":7858,"depth":173,"text":7859},{"id":2566,"depth":173,"text":2567},"For a long time, Vue as a framework had a built-in feature of binding inline styles in HTML templates. But what has changed since the new Vue 3.2 release is that we can now also use the existing v-bind syntax with reactive variables inside our \u003Cstyle> tag in the .vue files aka Single File Components (SFC). Let's take a look how can we do that. With the two examples above, we saw how easy it was to implement a style binding using v-bind syntax in Vue 3.2. But the question is, how has Vue allowed for this? If we inspect the elements in the DevTools of the browser, we will see that all elements used v-bind have their inline styles with CSS variable in them.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1631578776\u002Fblog\u002Fstyle-binding-in-sfc-in-vue-3\u002Fstyle-binding-in-sfc-in-vue-3",[7529,7568,8448,8449,8450,5299,5300],"Vue 3","Binding Styles in Vue","CSS Variables",{},"\u002F2021\u002F09\u002F14\u002Fstyle-binding-in-sfc-in-vue-3","14th Sep 2021",{"title":7529,"description":8445},"2021\u002F09\u002F14\u002Fstyle-binding-in-sfc-in-vue-3","wAVogG7pcyF5YUEvZ_FInbq1KbPOAMwBO3lkfe3ZGNI",{"id":8458,"title":8459,"articleTags":8460,"author":11,"blog":12,"body":8461,"description":9457,"extension":2649,"image":9458,"keywords":9459,"meta":9463,"navigation":515,"path":9464,"published":9465,"readTime":302,"seo":9466,"stem":9467,"type":2662,"__hash__":9468},"content\u002F2021\u002F09\u002F17\u002Fincluding-local-nodejs-modules-in-firebase-cloud-functions.md","Including local Node.js modules in Firebase Cloud Functions",[9,2668,2669],{"type":14,"value":8462,"toc":9450},[8463,8466,8480,8482,8486,8491,8494,8497,8501,8504,8661,8663,8698,8701,8707,8713,8733,8742,8869,8890,8895,8944,8947,8951,8962,8968,8975,8987,8992,8995,9000,9007,9012,9022,9046,9049,9067,9184,9205,9329,9343,9379,9390,9416,9422,9424,9441,9447],[17,8464,8459],{"id":8465},"including-local-nodejs-modules-in-firebase-cloud-functions",[27,8467,8468],{},[30,8469,8470,36,8472,40,8474],{},[33,8471],{"value":35},[33,8473],{"value":39},[42,8475,8476],{},[45,8477,8478],{"href":47},[33,8479],{"value":50},[52,8481],{":tags":54},[56,8483],{":audio-src":8484,":transcript-src":8485},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F09\u002F17\u002Fincluding-local-nodejs-modules-in-firebase-cloud-functions\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F09\u002F17\u002Fincluding-local-nodejs-modules-in-firebase-cloud-functions\u002Fsummary.json",[27,8487,8488],{},[63,8489],{"alt":65,"src":8490},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1631602643\u002Fblog\u002Fincluding-local-nodejs-modules-in-firebase-cloud-functions\u002Fincluding-local-modules",[27,8492,8493],{},"Modules have become a major part of the JavaScript ecosystem. They allow us to split our JavaScript programs up into separate smaller parts, called modules, that can be imported when needed.",[27,8495,8496],{},"In this article, we will have a look at how we can include local modules in our Node.js app, and then see how that differentiates from including them in Firebase Cloud Functions. Before we continue, do note that this is not an introduction to Firebase. A basic understanding of Firebase and JavaScript is required before reading on about the examples that I'm about to explain.",[104,8498,8500],{"id":8499},"build-simple-app-in-nodejs","Build simple app in Node.js",[27,8502,8503],{},"Before diving into Node.js modules, let's build a simple Node.js app. We are going to create a function that takes a string and then returns a capitalised first letter of each word in that string. Let's see how we can do that.",[128,8505,8507],{"className":130,"code":8506,"language":132,"meta":133,"style":133},"function capitalise(input) {\n    const words = input.split(\" \");\n    const CapitalisedWords = [];\n    words.forEach((word) => {\n        CapitalisedWords.push(word[0].toUpperCase() + word.slice(1, word.length));\n    });\n    return CapitalisedWords.join(\" \");\n}\n\nconsole.log(capitalise(\"javascript engines were originally used only in web browsers\"));\n",[22,8508,8509,8523,8545,8557,8576,8615,8619,8635,8639,8643],{"__ignoreMap":133},[137,8510,8511,8513,8516,8518,8521],{"class":139,"line":140},[137,8512,483],{"class":143},[137,8514,8515],{"class":147}," capitalise",[137,8517,356],{"class":157},[137,8519,8520],{"class":161},"input",[137,8522,170],{"class":157},[137,8524,8525,8527,8530,8532,8535,8538,8540,8543],{"class":139,"line":173},[137,8526,4177],{"class":143},[137,8528,8529],{"class":364}," words",[137,8531,151],{"class":143},[137,8533,8534],{"class":157}," input.",[137,8536,8537],{"class":147},"split",[137,8539,356],{"class":157},[137,8541,8542],{"class":284},"\" \"",[137,8544,1502],{"class":157},[137,8546,8547,8549,8552,8554],{"class":139,"line":188},[137,8548,4177],{"class":143},[137,8550,8551],{"class":364}," CapitalisedWords",[137,8553,151],{"class":143},[137,8555,8556],{"class":157}," [];\n",[137,8558,8559,8562,8565,8567,8570,8572,8574],{"class":139,"line":269},[137,8560,8561],{"class":157},"    words.",[137,8563,8564],{"class":147},"forEach",[137,8566,2774],{"class":157},[137,8568,8569],{"class":161},"word",[137,8571,219],{"class":157},[137,8573,222],{"class":143},[137,8575,256],{"class":157},[137,8577,8578,8581,8584,8587,8589,8591,8593,8595,8597,8600,8602,8604,8606,8609,8612],{"class":139,"line":278},[137,8579,8580],{"class":157},"        CapitalisedWords.",[137,8582,8583],{"class":147},"push",[137,8585,8586],{"class":157},"(word[",[137,8588,6044],{"class":364},[137,8590,6047],{"class":157},[137,8592,6050],{"class":147},[137,8594,3348],{"class":157},[137,8596,182],{"class":143},[137,8598,8599],{"class":157}," word.",[137,8601,6060],{"class":147},[137,8603,356],{"class":157},[137,8605,6065],{"class":364},[137,8607,8608],{"class":157},", word.",[137,8610,8611],{"class":364},"length",[137,8613,8614],{"class":157},"));\n",[137,8616,8617],{"class":139,"line":291},[137,8618,2832],{"class":157},[137,8620,8621,8623,8626,8629,8631,8633],{"class":139,"line":297},[137,8622,176],{"class":143},[137,8624,8625],{"class":157}," CapitalisedWords.",[137,8627,8628],{"class":147},"join",[137,8630,356],{"class":157},[137,8632,8542],{"class":284},[137,8634,1502],{"class":157},[137,8636,8637],{"class":139,"line":302},[137,8638,510],{"class":157},[137,8640,8641],{"class":139,"line":662},[137,8642,516],{"emptyLinePlaceholder":515},[137,8644,8645,8647,8649,8651,8654,8656,8659],{"class":139,"line":667},[137,8646,1436],{"class":157},[137,8648,353],{"class":147},[137,8650,356],{"class":157},[137,8652,8653],{"class":147},"capitalise",[137,8655,356],{"class":157},[137,8657,8658],{"class":284},"\"javascript engines were originally used only in web browsers\"",[137,8660,8614],{"class":157},[27,8662,5630],{},[128,8664,8668],{"className":8665,"code":8666,"language":8667,"meta":133,"style":133},"language-bash shiki shiki-themes github-light github-dark","Javascript Engines Were Originally Used Only In Web Browsers\n","bash",[22,8669,8670],{"__ignoreMap":133},[137,8671,8672,8674,8677,8680,8683,8686,8689,8692,8695],{"class":139,"line":140},[137,8673,2652],{"class":147},[137,8675,8676],{"class":284}," Engines",[137,8678,8679],{"class":284}," Were",[137,8681,8682],{"class":284}," Originally",[137,8684,8685],{"class":284}," Used",[137,8687,8688],{"class":284}," Only",[137,8690,8691],{"class":284}," In",[137,8693,8694],{"class":284}," Web",[137,8696,8697],{"class":284}," Browsers\n",[27,8699,8700],{},"The example above works perfectly fine, but it would be nicer if we split it into smaller peaces. Let's carry this out.",[104,8702,8704,8705,154],{"id":8703},"create-a-module-from-the-capitalise-function","Create a module from the ",[22,8706,8653],{},[27,8708,8709,8710,8712],{},"To organise our code better, we can define a separate module with the ",[22,8711,8653],{}," function, and then include this in our main app. Our directory structure should look like this:",[128,8714,8716],{"className":5633,"code":8715,"language":5635,"meta":133,"style":133},"root\u002F\n  index.js\n  capitalise.js\n",[22,8717,8718,8723,8728],{"__ignoreMap":133},[137,8719,8720],{"class":139,"line":140},[137,8721,8722],{},"root\u002F\n",[137,8724,8725],{"class":139,"line":173},[137,8726,8727],{},"  index.js\n",[137,8729,8730],{"class":139,"line":188},[137,8731,8732],{},"  capitalise.js\n",[27,8734,4286,8735,8738,8739,8741],{},[22,8736,8737],{},"capitalise.js"," we define ",[22,8740,8653],{}," function as follows:",[128,8743,8745],{"className":130,"code":8744,"language":132,"meta":133,"style":133},"module.exports.capitalise = function (input) {\n    const words = input.split(\" \");\n    const CapitalisedWords = [];\n    words.forEach((word) => {\n        CapitalisedWords.push(word[0].toUpperCase() + word.slice(1, word.length));\n    });\n    return CapitalisedWords.join(\" \");\n};\n",[22,8746,8747,8771,8789,8799,8815,8847,8851,8865],{"__ignoreMap":133},[137,8748,8749,8752,8754,8757,8759,8761,8763,8765,8767,8769],{"class":139,"line":140},[137,8750,8751],{"class":364},"module",[137,8753,1017],{"class":157},[137,8755,8756],{"class":364},"exports",[137,8758,1017],{"class":157},[137,8760,8653],{"class":147},[137,8762,151],{"class":143},[137,8764,154],{"class":143},[137,8766,158],{"class":157},[137,8768,8520],{"class":161},[137,8770,170],{"class":157},[137,8772,8773,8775,8777,8779,8781,8783,8785,8787],{"class":139,"line":173},[137,8774,4177],{"class":143},[137,8776,8529],{"class":364},[137,8778,151],{"class":143},[137,8780,8534],{"class":157},[137,8782,8537],{"class":147},[137,8784,356],{"class":157},[137,8786,8542],{"class":284},[137,8788,1502],{"class":157},[137,8790,8791,8793,8795,8797],{"class":139,"line":188},[137,8792,4177],{"class":143},[137,8794,8551],{"class":364},[137,8796,151],{"class":143},[137,8798,8556],{"class":157},[137,8800,8801,8803,8805,8807,8809,8811,8813],{"class":139,"line":269},[137,8802,8561],{"class":157},[137,8804,8564],{"class":147},[137,8806,2774],{"class":157},[137,8808,8569],{"class":161},[137,8810,219],{"class":157},[137,8812,222],{"class":143},[137,8814,256],{"class":157},[137,8816,8817,8819,8821,8823,8825,8827,8829,8831,8833,8835,8837,8839,8841,8843,8845],{"class":139,"line":278},[137,8818,8580],{"class":157},[137,8820,8583],{"class":147},[137,8822,8586],{"class":157},[137,8824,6044],{"class":364},[137,8826,6047],{"class":157},[137,8828,6050],{"class":147},[137,8830,3348],{"class":157},[137,8832,182],{"class":143},[137,8834,8599],{"class":157},[137,8836,6060],{"class":147},[137,8838,356],{"class":157},[137,8840,6065],{"class":364},[137,8842,8608],{"class":157},[137,8844,8611],{"class":364},[137,8846,8614],{"class":157},[137,8848,8849],{"class":139,"line":291},[137,8850,2832],{"class":157},[137,8852,8853,8855,8857,8859,8861,8863],{"class":139,"line":297},[137,8854,176],{"class":143},[137,8856,8625],{"class":157},[137,8858,8628],{"class":147},[137,8860,356],{"class":157},[137,8862,8542],{"class":284},[137,8864,1502],{"class":157},[137,8866,8867],{"class":139,"line":302},[137,8868,191],{"class":157},[27,8870,8871,8872,8875,8876,8881,8882,8885,8886,8889],{},"And then in ",[22,8873,8874],{},"index.js"," we include the capitalise module and call that function. We use the ",[45,8877,8880],{"href":8878,"target":2716,"rel":8879},"https:\u002F\u002Frequirejs.org\u002Fdocs\u002Fcommonjs.html",[2718,2719],"CommonJS"," syntax to export and import modules in our Node.js app. In CommonJS ",[22,8883,8884],{},"module.exports"," method is used for exporting modules, and ",[22,8887,8888],{},"require()"," method for importing.",[3244,8891,8892],{},[27,8893,8894],{},"Please note that with the release of Node version 15.3.0 ES modules can also be used.",[128,8896,8898],{"className":130,"code":8897,"language":132,"meta":133,"style":133},"const { capitalise } = require(\".\u002Fcapitalise\");\n\nconsole.log(capitalise(\"javascript engines were originally used only in web browsers\"));\n",[22,8899,8900,8924,8928],{"__ignoreMap":133},[137,8901,8902,8904,8907,8909,8912,8914,8917,8919,8922],{"class":139,"line":140},[137,8903,3077],{"class":143},[137,8905,8906],{"class":157}," { ",[137,8908,8653],{"class":364},[137,8910,8911],{"class":157}," } ",[137,8913,253],{"class":143},[137,8915,8916],{"class":147}," require",[137,8918,356],{"class":157},[137,8920,8921],{"class":284},"\".\u002Fcapitalise\"",[137,8923,1502],{"class":157},[137,8925,8926],{"class":139,"line":173},[137,8927,516],{"emptyLinePlaceholder":515},[137,8929,8930,8932,8934,8936,8938,8940,8942],{"class":139,"line":188},[137,8931,1436],{"class":157},[137,8933,353],{"class":147},[137,8935,356],{"class":157},[137,8937,8653],{"class":147},[137,8939,356],{"class":157},[137,8941,8658],{"class":284},[137,8943,8614],{"class":157},[27,8945,8946],{},"We've achieved the exact same outcome now by using modules. As we can see, modules help to better organise and structure our codebase.",[104,8948,8950],{"id":8949},"create-the-same-app-in-firebase-cloud-functions","Create the same app in Firebase Cloud Functions",[27,8952,8953,8954,8957,8958,1017],{},"Before we start working with Firebase, we need to create a Firebase project in the ",[45,8955,2720],{"href":2715,"target":2716,"rel":8956},[2718,2719],". In order to be able to use Firebase Cloud Functions, we need to upgrade to the Blaze plan (pay as you go). Firebase has a moderate free tier for hobby users. If you'ld like to check the prices for their services visit the following ",[45,8959,2726],{"href":8960,"target":2716,"rel":8961},"https:\u002F\u002Ffirebase.google.com\u002Fpricing",[2718,2719],[27,8963,8964,8965,1017],{},"The next step is to install Firebase command line tools using npm. We can install the firebase tools with the following command ",[22,8966,8967],{},"npm install -g firebase-tools",[27,8969,8970,8971,8974],{},"Once firebase tools have been installed, we need to login to our firebase. We can do so with the following command in the terminal ",[22,8972,8973],{},"firebase login"," and then visit the URL that firebase CLI will output in the command line.",[27,8976,8977,8978,8981,8982],{},"In our root directory, we will now initialise our firebase functions. We initialise our functions with ",[22,8979,8980],{},"firebase init",". Firebase CLI will give us options to select. We need to chose ",[42,8983,8984],{},[30,8985,8986],{},"Functions: Configure a Cloud Functions directory and its files.",[27,8988,8989],{},[63,8990],{"alt":65,"src":8991},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1631602642\u002Fblog\u002Fincluding-local-nodejs-modules-in-firebase-cloud-functions\u002Fimage-1_nxs6lz",[27,8993,8994],{},"We next choose the project that we've created earlier in the firebase console:",[27,8996,8997],{},[63,8998],{"alt":65,"src":8999},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1631602642\u002Fblog\u002Fincluding-local-nodejs-modules-in-firebase-cloud-functions\u002Fimage-2_bm8o63",[27,9001,9002,9003,9006],{},"Select JavaScript as a language, ",[42,9004,9005],{},"No"," to ESLint, and lastly choose to install dependencies with npm.",[27,9008,9009],{},[63,9010],{"alt":65,"src":9011},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1631602643\u002Fblog\u002Fincluding-local-nodejs-modules-in-firebase-cloud-functions\u002Fimage-3_rbawf6",[27,9013,9014,9015,9018,9019,9021],{},"Now we have ",[42,9016,9017],{},"functions"," folder with ",[42,9020,8874],{}," file. Our directory structure should look like this:",[128,9023,9025],{"className":5633,"code":9024,"language":5635,"meta":133,"style":133},"root\u002F\n  functions\u002F\n      index.js\n        package.json\n",[22,9026,9027,9031,9036,9041],{"__ignoreMap":133},[137,9028,9029],{"class":139,"line":140},[137,9030,8722],{},[137,9032,9033],{"class":139,"line":173},[137,9034,9035],{},"  functions\u002F\n",[137,9037,9038],{"class":139,"line":188},[137,9039,9040],{},"      index.js\n",[137,9042,9043],{"class":139,"line":269},[137,9044,9045],{},"        package.json\n",[27,9047,9048],{},"The way we include local Node.js modules in our cloud functions is a little different than our earlier Node.js app. If we follow the earlier steps, we will end up with an error when we try to deploy Cloud Functions to Firebase. In order to include local Node.js modules in cloud functions, we need to to do the following.",[27,9050,4286,9051,9053,9054,9057,9058,9060,9061,2107,9064,9066],{},[42,9052,8874],{}," we will now declare our ",[22,9055,9056],{},"onRequest"," cloud function and then include our capitalise module. You can clearly see that we call ",[22,9059,8653],{}," instead of ",[22,9062,9063],{},".\u002Fcapitalise",[22,9065,8888],{}," method.",[128,9068,9070],{"className":130,"code":9069,"language":132,"meta":133,"style":133},"const functions = require(\"firebase-functions\");\nconst { capitalise } = require(\"capitalise\");\n\nexports.capitaliseWords = functions.https.onRequest((req, res) => {\n    const text = capitalise(req.body.text);\n    res.status(200).send(text);\n});\n",[22,9071,9072,9090,9111,9115,9145,9159,9180],{"__ignoreMap":133},[137,9073,9074,9076,9079,9081,9083,9085,9088],{"class":139,"line":140},[137,9075,3077],{"class":143},[137,9077,9078],{"class":364}," functions",[137,9080,151],{"class":143},[137,9082,8916],{"class":147},[137,9084,356],{"class":157},[137,9086,9087],{"class":284},"\"firebase-functions\"",[137,9089,1502],{"class":157},[137,9091,9092,9094,9096,9098,9100,9102,9104,9106,9109],{"class":139,"line":173},[137,9093,3077],{"class":143},[137,9095,8906],{"class":157},[137,9097,8653],{"class":364},[137,9099,8911],{"class":157},[137,9101,253],{"class":143},[137,9103,8916],{"class":147},[137,9105,356],{"class":157},[137,9107,9108],{"class":284},"\"capitalise\"",[137,9110,1502],{"class":157},[137,9112,9113],{"class":139,"line":188},[137,9114,516],{"emptyLinePlaceholder":515},[137,9116,9117,9119,9122,9124,9127,9129,9131,9134,9136,9139,9141,9143],{"class":139,"line":269},[137,9118,8756],{"class":364},[137,9120,9121],{"class":157},".capitaliseWords ",[137,9123,253],{"class":143},[137,9125,9126],{"class":157}," functions.https.",[137,9128,9056],{"class":147},[137,9130,2774],{"class":157},[137,9132,9133],{"class":161},"req",[137,9135,164],{"class":157},[137,9137,9138],{"class":161},"res",[137,9140,219],{"class":157},[137,9142,222],{"class":143},[137,9144,256],{"class":157},[137,9146,9147,9149,9152,9154,9156],{"class":139,"line":278},[137,9148,4177],{"class":143},[137,9150,9151],{"class":364}," text",[137,9153,151],{"class":143},[137,9155,8515],{"class":147},[137,9157,9158],{"class":157},"(req.body.text);\n",[137,9160,9161,9164,9167,9169,9172,9174,9177],{"class":139,"line":291},[137,9162,9163],{"class":157},"    res.",[137,9165,9166],{"class":147},"status",[137,9168,356],{"class":157},[137,9170,9171],{"class":364},"200",[137,9173,4409],{"class":157},[137,9175,9176],{"class":147},"send",[137,9178,9179],{"class":157},"(text);\n",[137,9181,9182],{"class":139,"line":297},[137,9183,5422],{"class":157},[27,9185,9186,9187,9190,9191,9194,9195,9197,9198,9201,9202,9204],{},"Next, we create a new directory inside our ",[42,9188,9189],{},"function\u002F"," folder called ",[42,9192,9193],{},"capitalise\u002F,"," with a sub-folder file called ",[42,9196,8874],{},". In the ",[42,9199,9200],{},"functions\u002Fcapitalise\u002Findex.js"," file we declare our ",[22,9203,8653],{}," functions and export as a module:",[128,9206,9207],{"className":130,"code":8744,"language":132,"meta":133,"style":133},[22,9208,9209,9231,9249,9259,9275,9307,9311,9325],{"__ignoreMap":133},[137,9210,9211,9213,9215,9217,9219,9221,9223,9225,9227,9229],{"class":139,"line":140},[137,9212,8751],{"class":364},[137,9214,1017],{"class":157},[137,9216,8756],{"class":364},[137,9218,1017],{"class":157},[137,9220,8653],{"class":147},[137,9222,151],{"class":143},[137,9224,154],{"class":143},[137,9226,158],{"class":157},[137,9228,8520],{"class":161},[137,9230,170],{"class":157},[137,9232,9233,9235,9237,9239,9241,9243,9245,9247],{"class":139,"line":173},[137,9234,4177],{"class":143},[137,9236,8529],{"class":364},[137,9238,151],{"class":143},[137,9240,8534],{"class":157},[137,9242,8537],{"class":147},[137,9244,356],{"class":157},[137,9246,8542],{"class":284},[137,9248,1502],{"class":157},[137,9250,9251,9253,9255,9257],{"class":139,"line":188},[137,9252,4177],{"class":143},[137,9254,8551],{"class":364},[137,9256,151],{"class":143},[137,9258,8556],{"class":157},[137,9260,9261,9263,9265,9267,9269,9271,9273],{"class":139,"line":269},[137,9262,8561],{"class":157},[137,9264,8564],{"class":147},[137,9266,2774],{"class":157},[137,9268,8569],{"class":161},[137,9270,219],{"class":157},[137,9272,222],{"class":143},[137,9274,256],{"class":157},[137,9276,9277,9279,9281,9283,9285,9287,9289,9291,9293,9295,9297,9299,9301,9303,9305],{"class":139,"line":278},[137,9278,8580],{"class":157},[137,9280,8583],{"class":147},[137,9282,8586],{"class":157},[137,9284,6044],{"class":364},[137,9286,6047],{"class":157},[137,9288,6050],{"class":147},[137,9290,3348],{"class":157},[137,9292,182],{"class":143},[137,9294,8599],{"class":157},[137,9296,6060],{"class":147},[137,9298,356],{"class":157},[137,9300,6065],{"class":364},[137,9302,8608],{"class":157},[137,9304,8611],{"class":364},[137,9306,8614],{"class":157},[137,9308,9309],{"class":139,"line":291},[137,9310,2832],{"class":157},[137,9312,9313,9315,9317,9319,9321,9323],{"class":139,"line":297},[137,9314,176],{"class":143},[137,9316,8625],{"class":157},[137,9318,8628],{"class":147},[137,9320,356],{"class":157},[137,9322,8542],{"class":284},[137,9324,1502],{"class":157},[137,9326,9327],{"class":139,"line":302},[137,9328,191],{"class":157},[27,9330,9331,9332,9335,9336,9339,9340,9342],{},"We will also need to run ",[42,9333,9334],{},"npm init -y"," inside ",[42,9337,9338],{},"functions\u002Fcapitalise\u002F"," directory. That way, we can initialise a ",[42,9341,5140],{}," file. Our final directory structure should look like this:",[128,9344,9346],{"className":5633,"code":9345,"language":5635,"meta":133,"style":133},"root\u002F\n  functions\u002F\n      index.js\n        package.json\n        capitalise\u002F\n            index.js\n            package.json\n",[22,9347,9348,9352,9356,9360,9364,9369,9374],{"__ignoreMap":133},[137,9349,9350],{"class":139,"line":140},[137,9351,8722],{},[137,9353,9354],{"class":139,"line":173},[137,9355,9035],{},[137,9357,9358],{"class":139,"line":188},[137,9359,9040],{},[137,9361,9362],{"class":139,"line":269},[137,9363,9045],{},[137,9365,9366],{"class":139,"line":278},[137,9367,9368],{},"        capitalise\u002F\n",[137,9370,9371],{"class":139,"line":291},[137,9372,9373],{},"            index.js\n",[137,9375,9376],{"class":139,"line":297},[137,9377,9378],{},"            package.json\n",[27,9380,9381,9382,9385,9386,9389],{},"Lastly, we need to declare our module in ",[42,9383,9384],{},"functions\u002Fpackage.json"," using the file: prefix. In the following example, capitalise refers to our module name and ",[42,9387,9388],{},"\"file:.\u002Fcapitalise\u002F\""," is the directory containing our module:",[128,9391,9393],{"className":5155,"code":9392,"language":5157,"meta":133,"style":133},"\"dependencies\": {\n        \"capitalise\": \"file:.\u002Fcapitalise\u002F\"\n    },\n",[22,9394,9395,9402,9412],{"__ignoreMap":133},[137,9396,9397,9400],{"class":139,"line":140},[137,9398,9399],{"class":284},"\"dependencies\"",[137,9401,1819],{"class":157},[137,9403,9404,9407,9409],{"class":139,"line":173},[137,9405,9406],{"class":364},"        \"capitalise\"",[137,9408,726],{"class":157},[137,9410,9411],{"class":284},"\"file:.\u002Fcapitalise\u002F\"\n",[137,9413,9414],{"class":139,"line":188},[137,9415,775],{"class":157},[27,9417,9418,9419,1017],{},"To deploy our cloud functions, we use the following command in our terminal ",[22,9420,9421],{},"firebase deploy --only functions",[104,9423,2567],{"id":2566},[2569,9425,9426,9429,9432,9435],{},[1006,9427,9428],{},"Modules have become a major part of the JavaScript ecosystem. They allow us to split our JavaScript programs up into separate smaller parts, so called modules that can be imported when needed.",[1006,9430,9431],{},"In Node.js, we organise our code better by defining separate local modules and then include them in our app.",[1006,9433,9434],{},"There are differences on how we include local modules in Node.js app versus Firebase Cloud functions.",[1006,9436,9437,9438,9440],{},"In order to use local modules in Firebase Cloud Functions, we need to declare our module in ",[42,9439,9384],{}," using the file: prefix.",[27,9442,5276,9443,1017],{},[45,9444,2726],{"href":9445,"target":2716,"rel":9446},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Flocal-nodejs-modules-in-firebase-cloud-functions",[2718,2719],[2617,9448,9449],{},"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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":133,"searchDepth":173,"depth":173,"links":9451},[9452,9453,9455,9456],{"id":8499,"depth":173,"text":8500},{"id":8703,"depth":173,"text":9454},"Create a module from the capitalise function",{"id":8949,"depth":173,"text":8950},{"id":2566,"depth":173,"text":2567},"Modules have become a major part of the JavaScript ecosystem. They allow us to split our JavaScript programs up into separate smaller parts, called modules, that can be imported when needed. In this article, we will have a look at how we can include local modules in our Node.js app, and then see how that differentiates from including them in Firebase Cloud Functions. Before we continue, do note that this is not an introduction to Firebase. A basic understanding of Firebase and JavaScript is required before reading on about the examples that I'm about to explain.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1631602643\u002Fblog\u002Fincluding-local-nodejs-modules-in-firebase-cloud-functions\u002Fincluding-local-modules",[8459,2669,2668,9460,9461,8880,9462,5299,5300],"JavaScript Modules","Common.js","Include Modules",{},"\u002F2021\u002F09\u002F17\u002Fincluding-local-nodejs-modules-in-firebase-cloud-functions","17th Sep 2021",{"title":8459,"description":9457},"2021\u002F09\u002F17\u002Fincluding-local-nodejs-modules-in-firebase-cloud-functions","-QU2NvILz6cBPDHE-S-2CZda79fziaW6QV_Ydok3das",{"id":9470,"title":9471,"articleTags":9472,"author":11,"blog":12,"body":9473,"description":10142,"extension":2649,"image":10143,"keywords":10144,"meta":10147,"navigation":515,"path":10148,"published":10149,"readTime":291,"seo":10150,"stem":10151,"type":2662,"__hash__":10152},"content\u002F2021\u002F09\u002F25\u002Fsuspense-feature-in-vue-3-with-sfc-script-setup.md","Suspense feature in Vue 3 with SFC Script Setup",[9,8,10],{"type":14,"value":9474,"toc":10134},[9475,9478,9492,9494,9498,9503,9509,9515,9520,9524,9527,9546,9549,9582,9586,9599,9629,9714,9723,9730,9750,9773,9876,9888,9891,9895,9902,10068,10084,10089,10091,10124,10131],[17,9476,9471],{"id":9477},"suspense-feature-in-vue-3-with-sfc-script-setup",[27,9479,9480],{},[30,9481,9482,36,9484,40,9486],{},[33,9483],{"value":35},[33,9485],{"value":39},[42,9487,9488],{},[45,9489,9490],{"href":47},[33,9491],{"value":50},[52,9493],{":tags":54},[56,9495],{":audio-src":9496,":transcript-src":9497},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F09\u002F25\u002Fsuspense-feature-in-vue-3-with-sfc-script-setup\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2021\u002F09\u002F25\u002Fsuspense-feature-in-vue-3-with-sfc-script-setup\u002Fsummary.json",[27,9499,9500],{},[63,9501],{"alt":65,"src":9502},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1632533795\u002Fblog\u002Fvue3-suspense\u002Fvue3-suspence-cover",[27,9504,9505,9508],{},[22,9506,9507],{},"\u003CSuspense>"," is a special component in Vue 3 that lets us wait for some data to be loaded, before our component can be rendered. In other words, Suspense allows us to render some fallback content. A good example will be a loading spinner while waiting for an asynchronous API call to fetch some data from the server. Once the data has been loaded, the main content will show up. This feature allows us to create a smooth user experience.",[27,9510,9511,9512,9514],{},"Let me demonstrate an example of how to use ",[22,9513,9507],{}," in Vue 3.",[3244,9516,9517],{},[27,9518,9519],{},"Note: Suspense is still an experimental new feature and the API could change down the track.",[104,9521,9523],{"id":9522},"create-a-vue-3-app-using-vite","Create a Vue 3 app using Vite",[27,9525,9526],{},"We can easily scaffolding our Vue 3 project using Vite with the following command:",[128,9528,9530],{"className":8665,"code":9529,"language":8667,"meta":133,"style":133},"npm init vite@latest my-vue-app\n",[22,9531,9532],{"__ignoreMap":133},[137,9533,9534,9537,9540,9543],{"class":139,"line":140},[137,9535,9536],{"class":147},"npm",[137,9538,9539],{"class":284}," init",[137,9541,9542],{"class":284}," vite@latest",[137,9544,9545],{"class":284}," my-vue-app\n",[27,9547,9548],{},"Next, we follow the prompts and choose Vue with JavaScript. Lastly, we install the dependency and run the project locally:",[128,9550,9552],{"className":8665,"code":9551,"language":8667,"meta":133,"style":133},"cd my-vue-app\n\nnpm install\nnpm run dev\n",[22,9553,9554,9561,9565,9572],{"__ignoreMap":133},[137,9555,9556,9559],{"class":139,"line":140},[137,9557,9558],{"class":364},"cd",[137,9560,9545],{"class":284},[137,9562,9563],{"class":139,"line":173},[137,9564,516],{"emptyLinePlaceholder":515},[137,9566,9567,9569],{"class":139,"line":188},[137,9568,9536],{"class":147},[137,9570,9571],{"class":284}," install\n",[137,9573,9574,9576,9579],{"class":139,"line":269},[137,9575,9536],{"class":147},[137,9577,9578],{"class":284}," run",[137,9580,9581],{"class":284}," dev\n",[104,9583,9585],{"id":9584},"make-an-asynchronous-api-call","Make an asynchronous API call",[27,9587,9588,9589,9592,9593,9598],{},"We are going to use the new ",[22,9590,9591],{},"\u003Cscript setup>"," syntax using ",[45,9594,9597],{"href":9595,"target":2716,"rel":9596},"https:\u002F\u002Fv3.vuejs.org\u002Fapi\u002Fcomposition-api.html",[2718,2719],"Composition API",". This feature was released with Vue 3.2.",[27,9600,9601,9602,9607,9608,9613,9614,9619,9620,9623,9624,9628],{},"First, create a new component called ",[42,9603,9604],{},[30,9605,9606],{},"Result.vue"," in ",[42,9609,9610],{},[30,9611,9612],{},"src\u002Fcomponents\u002FResult.vue"," directory. Next, use the ",[45,9615,9618],{"href":9616,"target":2716,"rel":9617},"https:\u002F\u002Fopenlibrary.org\u002Fdevelopers\u002Fapi",[2718,2719],"OpenLibrary"," API to fetch all ",[42,9621,9622],{},"The Lord Of The Rings"," books. Let's look at the following code in ",[42,9625,9626],{},[30,9627,9606],{}," component:",[128,9630,9632],{"className":130,"code":9631,"language":132,"meta":133,"style":133},"\u003Cscript setup>\n    import { ref } from \"vue\";\n\n    const response = await fetch(\"http:\u002F\u002Fopenlibrary.org\u002Fsearch.json?q=the+lord+of+the+rings\");\n    const data = await response.json();\n\n    const msg = ref(`It has been found: ${data.num_found} results!`);\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Ch1>{{ msg }}\u003C\u002Fh1>\n\u003C\u002Ftemplate>\n",[22,9633,9634,9645,9650,9654,9659,9664,9668,9673,9681,9685,9693,9706],{"__ignoreMap":133},[137,9635,9636,9638,9640,9643],{"class":139,"line":140},[137,9637,4033],{"class":157},[137,9639,4037],{"class":4036},[137,9641,9642],{"class":147}," setup",[137,9644,4053],{"class":157},[137,9646,9647],{"class":139,"line":173},[137,9648,9649],{"class":157},"    import { ref } from \"vue\";\n",[137,9651,9652],{"class":139,"line":188},[137,9653,516],{"emptyLinePlaceholder":515},[137,9655,9656],{"class":139,"line":269},[137,9657,9658],{"class":157},"    const response = await fetch(\"http:\u002F\u002Fopenlibrary.org\u002Fsearch.json?q=the+lord+of+the+rings\");\n",[137,9660,9661],{"class":139,"line":278},[137,9662,9663],{"class":157},"    const data = await response.json();\n",[137,9665,9666],{"class":139,"line":291},[137,9667,516],{"emptyLinePlaceholder":515},[137,9669,9670],{"class":139,"line":297},[137,9671,9672],{"class":157},"    const msg = ref(`It has been found: ${data.num_found} results!`);\n",[137,9674,9675,9677,9679],{"class":139,"line":302},[137,9676,4083],{"class":157},[137,9678,4037],{"class":4036},[137,9680,4053],{"class":157},[137,9682,9683],{"class":139,"line":662},[137,9684,516],{"emptyLinePlaceholder":515},[137,9686,9687,9689,9691],{"class":139,"line":667},[137,9688,4033],{"class":157},[137,9690,7821],{"class":4036},[137,9692,4053],{"class":157},[137,9694,9695,9697,9699,9702,9704],{"class":139,"line":786},[137,9696,4072],{"class":157},[137,9698,17],{"class":4036},[137,9700,9701],{"class":157},">{{ msg }}\u003C\u002F",[137,9703,17],{"class":4036},[137,9705,4053],{"class":157},[137,9707,9708,9710,9712],{"class":139,"line":798},[137,9709,4083],{"class":157},[137,9711,7821],{"class":4036},[137,9713,4053],{"class":157},[27,9715,4737,9716,9718,9719,9722],{},[22,9717,9591],{}," has top-level await, this means that the reactive variable ",[22,9720,9721],{},"msg"," won't be shown in the template until the fetch is completed. To improve the user experience, it would be nice if we displayed a loading message before the data has been loaded. This is where Suspense comes handy.",[104,9724,9726,9727,9729],{"id":9725},"use-suspense-component","Use ",[22,9728,9507],{}," component",[27,9731,4286,9732,9737,9738,9742,9743,9746,9747,9749],{},[42,9733,9734],{},[30,9735,9736],{},"App.vue"," file, we will import our ",[42,9739,9740],{},[30,9741,9606],{}," component and then use it in our template. We next wrap our ",[22,9744,9745],{},"\u003CResult \u002F>"," component with the special Vue ",[22,9748,9507],{},"component. Suspense is a built-in component in Vue 3. Hence, we do not need to import it.",[27,9751,4737,9752,9754,9755,114,9758,9761,9762,9764,9765,9767,9768,9772],{},[22,9753,9507],{}," component has two slots, ",[22,9756,9757],{},"default",[22,9759,9760],{},"fallback",". In our example below, while the content isn't ready, the ",[22,9763,9760],{}," component is rendered. When the data has been successfully loaded, the ",[22,9766,9757],{}," component is displayed. Sounds easy, doesn't it. Let's see our code in ",[42,9769,9770],{},[30,9771,9736],{}," file:",[128,9774,9776],{"className":130,"code":9775,"language":132,"meta":133,"style":133},"\u003Cscript setup>\n import Result from \".\u002Fcomponents\u002FResult.vue\";\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003CSuspense>\n        \u003Ctemplate #default>\n            \u003CResult \u002F>\n        \u003C\u002Ftemplate>\n        \u003Ctemplate #fallback>\n            \u003Ch1>Loading...\u003C\u002Fh1>\n        \u003C\u002Ftemplate>\n    \u003C\u002FSuspense>\n\u003C\u002Ftemplate>\n",[22,9777,9778,9788,9793,9801,9805,9813,9822,9832,9839,9848,9857,9862,9867,9872],{"__ignoreMap":133},[137,9779,9780,9782,9784,9786],{"class":139,"line":140},[137,9781,4033],{"class":157},[137,9783,4037],{"class":4036},[137,9785,9642],{"class":147},[137,9787,4053],{"class":157},[137,9789,9790],{"class":139,"line":173},[137,9791,9792],{"class":157}," import Result from \".\u002Fcomponents\u002FResult.vue\";\n",[137,9794,9795,9797,9799],{"class":139,"line":188},[137,9796,4083],{"class":157},[137,9798,4037],{"class":4036},[137,9800,4053],{"class":157},[137,9802,9803],{"class":139,"line":269},[137,9804,516],{"emptyLinePlaceholder":515},[137,9806,9807,9809,9811],{"class":139,"line":278},[137,9808,4033],{"class":157},[137,9810,7821],{"class":4036},[137,9812,4053],{"class":157},[137,9814,9815,9817,9820],{"class":139,"line":291},[137,9816,4072],{"class":157},[137,9818,9819],{"class":364},"Suspense",[137,9821,4053],{"class":157},[137,9823,9824,9827,9829],{"class":139,"line":297},[137,9825,9826],{"class":157},"        \u003C",[137,9828,7821],{"class":4036},[137,9830,9831],{"class":8180}," #default>\n",[137,9833,9834,9837],{"class":139,"line":302},[137,9835,9836],{"class":8180},"            \u003CResult",[137,9838,4078],{"class":157},[137,9840,9841,9844,9846],{"class":139,"line":662},[137,9842,9843],{"class":157},"        \u003C\u002F",[137,9845,7821],{"class":4036},[137,9847,4053],{"class":157},[137,9849,9850,9852,9854],{"class":139,"line":667},[137,9851,9826],{"class":157},[137,9853,7821],{"class":4036},[137,9855,9856],{"class":8180}," #fallback>\n",[137,9858,9859],{"class":139,"line":786},[137,9860,9861],{"class":8180},"            \u003Ch1>Loading...\u003C\u002Fh1>\n",[137,9863,9864],{"class":139,"line":798},[137,9865,9866],{"class":8180},"        \u003C\u002Ftemplate>\n",[137,9868,9869],{"class":139,"line":803},[137,9870,9871],{"class":8180},"    \u003C\u002FSuspense>\n",[137,9873,9874],{"class":139,"line":931},[137,9875,8189],{"class":8180},[27,9877,9878,9881,9882,9884,9885,1017],{},[42,9879,9880],{},"Loading..."," will be displayed while we wait for the fetch API to be completed. Once the data is loaded, ",[22,9883,9745],{}," component will be rendered. In our example, it will be displayed as ",[42,9886,9887],{},"It has been found: 487 results!",[27,9889,9890],{},"But what will happen if we have an error while fetching the data from the API? Currently, the fallback component will stay for an infinite amount of time as there is no error handler. So how can we then deal with errors?",[104,9892,9894],{"id":9893},"handling-errors","Handling Errors",[27,9896,9897,9898,9901],{},"Dealing with errors in Vue is simple. Vue 3 provides ",[22,9899,9900],{},"onErrorCaptured"," lifecycle hook that can listen to errors. Let's see how that would look like in our final code:",[128,9903,9905],{"className":130,"code":9904,"language":132,"meta":133,"style":133},"\u003Cscript setup>\nimport { ref, onErrorCaptured } from \"vue\";\nimport Result from \".\u002Fcomponents\u002FResult.vue\";\n\nconst error = ref(null);\n\nonErrorCaptured(() => {\n    error.value = \"Ohh! Something went wrong!\";\n});\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv v-if=\"error\">{{ error }}\u003C\u002Fdiv>\n    \u003CSuspense v-else>\n        \u003Ctemplate #default>\n            \u003CResult \u002F>\n        \u003C\u002Ftemplate>\n        \u003Ctemplate #fallback>\n            \u003Ch1>Loading...\u003C\u002Fh1>\n        \u003C\u002Ftemplate>\n    \u003C\u002FSuspense>\n\u003C\u002Ftemplate>\n\n\u003Cstyle>\n",[22,9906,9907,9917,9922,9927,9931,9936,9940,9945,9957,9961,9969,9973,9981,10002,10013,10021,10027,10035,10043,10047,10051,10055,10059,10063],{"__ignoreMap":133},[137,9908,9909,9911,9913,9915],{"class":139,"line":140},[137,9910,4033],{"class":157},[137,9912,4037],{"class":4036},[137,9914,9642],{"class":147},[137,9916,4053],{"class":157},[137,9918,9919],{"class":139,"line":173},[137,9920,9921],{"class":157},"import { ref, onErrorCaptured } from \"vue\";\n",[137,9923,9924],{"class":139,"line":188},[137,9925,9926],{"class":157},"import Result from \".\u002Fcomponents\u002FResult.vue\";\n",[137,9928,9929],{"class":139,"line":269},[137,9930,516],{"emptyLinePlaceholder":515},[137,9932,9933],{"class":139,"line":278},[137,9934,9935],{"class":157},"const error = ref(null);\n",[137,9937,9938],{"class":139,"line":291},[137,9939,516],{"emptyLinePlaceholder":515},[137,9941,9942],{"class":139,"line":297},[137,9943,9944],{"class":157},"onErrorCaptured(() => {\n",[137,9946,9947,9950,9952,9955],{"class":139,"line":302},[137,9948,9949],{"class":157},"    error.value ",[137,9951,253],{"class":143},[137,9953,9954],{"class":284}," \"Ohh! Something went wrong!\"",[137,9956,3276],{"class":157},[137,9958,9959],{"class":139,"line":662},[137,9960,5422],{"class":157},[137,9962,9963,9965,9967],{"class":139,"line":667},[137,9964,4083],{"class":157},[137,9966,4037],{"class":4036},[137,9968,4053],{"class":157},[137,9970,9971],{"class":139,"line":786},[137,9972,516],{"emptyLinePlaceholder":515},[137,9974,9975,9977,9979],{"class":139,"line":798},[137,9976,4033],{"class":157},[137,9978,7821],{"class":4036},[137,9980,4053],{"class":157},[137,9982,9983,9985,9987,9990,9992,9995,9998,10000],{"class":139,"line":803},[137,9984,4072],{"class":157},[137,9986,8330],{"class":4036},[137,9988,9989],{"class":147}," v-if",[137,9991,253],{"class":143},[137,9993,9994],{"class":284},"\"error\"",[137,9996,9997],{"class":157},">{{ error }}\u003C\u002F",[137,9999,8330],{"class":4036},[137,10001,4053],{"class":157},[137,10003,10004,10006,10008,10011],{"class":139,"line":931},[137,10005,4072],{"class":157},[137,10007,9819],{"class":364},[137,10009,10010],{"class":147}," v-else",[137,10012,4053],{"class":157},[137,10014,10015,10017,10019],{"class":139,"line":1568},[137,10016,9826],{"class":157},[137,10018,7821],{"class":4036},[137,10020,9831],{"class":8180},[137,10022,10023,10025],{"class":139,"line":1573},[137,10024,9836],{"class":8180},[137,10026,4078],{"class":157},[137,10028,10029,10031,10033],{"class":139,"line":1578},[137,10030,9843],{"class":157},[137,10032,7821],{"class":4036},[137,10034,4053],{"class":157},[137,10036,10037,10039,10041],{"class":139,"line":1588},[137,10038,9826],{"class":157},[137,10040,7821],{"class":4036},[137,10042,9856],{"class":8180},[137,10044,10045],{"class":139,"line":1601},[137,10046,9861],{"class":8180},[137,10048,10049],{"class":139,"line":3802},[137,10050,9866],{"class":8180},[137,10052,10053],{"class":139,"line":3808},[137,10054,9871],{"class":8180},[137,10056,10057],{"class":139,"line":3822},[137,10058,8189],{"class":8180},[137,10060,10061],{"class":139,"line":3827},[137,10062,516],{"emptyLinePlaceholder":515},[137,10064,10065],{"class":139,"line":3832},[137,10066,10067],{"class":8180},"\u003Cstyle>\n",[27,10069,10070,10071,10073,10074,10076,10077,10079,10080,10083],{},"In the above, we created ",[22,10072,9900],{}," hook and a reactive variable called ",[22,10075,2812],{},". Now when an error occurs, Vue hook will be able to capture it and assign value to the ",[22,10078,2812],{}," reactive variable. In our template, we have a condition, if the error exists then we display the error message: ",[42,10081,10082],{},"Ohh! Something went wrong!",", otherwise we continue with the Suspense component.",[27,10085,10086],{},[63,10087],{"alt":65,"src":10088},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1632533777\u002Fblog\u002Fvue3-suspense\u002Fvue3-suspense_mpqxff",[104,10090,2567],{"id":2566},[2569,10092,10093,10098,10104,10119],{},[1006,10094,10095,10097],{},[22,10096,9507],{}," is a special component in Vue 3 that lets us wait for some data to be loaded, before our component can be rendered.",[1006,10099,10100,10101,10103],{},"We can use the new ",[22,10102,9591],{}," syntax and its top-level await feature to asynchronously load data from the server.",[1006,10105,4737,10106,9754,10108,114,10110,10112,10113,10115,10116,10118],{},[22,10107,9507],{},[22,10109,9757],{},[22,10111,9760],{},". When the content isn't ready, the ",[22,10114,9760],{}," component is rendered. When data is successfully loaded, the ",[22,10117,9757],{}," component is displayed.",[1006,10120,9897,10121,10123],{},[22,10122,9900],{}," lifecycle hook that can listen to errors.",[27,10125,10126,10127,1017],{},"All examples above can be found in the following Github repository ",[45,10128,2726],{"href":10129,"target":2716,"rel":10130},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fsuspense-feature-in-vue-3",[2718,2719],[2617,10132,10133],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":133,"searchDepth":173,"depth":173,"links":10135},[10136,10137,10138,10140,10141],{"id":9522,"depth":173,"text":9523},{"id":9584,"depth":173,"text":9585},{"id":9725,"depth":173,"text":10139},"Use \u003CSuspense> component",{"id":9893,"depth":173,"text":9894},{"id":2566,"depth":173,"text":2567},"\u003CSuspense> is a special component in Vue 3 that lets us wait for some data to be loaded, before our component can be rendered. In other words, Suspense allows us to render some fallback content. A good example will be a loading spinner while waiting for an asynchronous API call to fetch some data from the server. Once the data has been loaded, the main content will show up. This feature allows us to create a smooth user experience. The \u003Cscript setup> has top-level await, this means that the reactive variable msg won't be shown in the template until the fetch is completed. To improve the user experience, it would be nice if we displayed a loading message before the data has been loaded. This is where Suspense comes handy.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1632533795\u002Fblog\u002Fvue3-suspense\u002Fvue3-suspence-cover",[9471,8,9819,9,10145,10146,5299,5300],"Loading","Show Spinner",{},"\u002F2021\u002F09\u002F25\u002Fsuspense-feature-in-vue-3-with-sfc-script-setup","25th Sep 2021",{"title":9471,"description":10142},"2021\u002F09\u002F25\u002Fsuspense-feature-in-vue-3-with-sfc-script-setup","awHdLhtCAe5DSlwkBN3PLh4gA6g3FbR9mrQ7rUw0HcA",{"id":10154,"title":10155,"articleTags":10156,"author":11,"blog":12,"body":10157,"description":12302,"extension":2649,"image":12303,"keywords":12304,"meta":12308,"navigation":515,"path":12309,"published":12310,"readTime":667,"seo":12311,"stem":12312,"type":2662,"__hash__":12313},"content\u002F2022\u002F04\u002F14\u002Fgsap-animation-with-vue3-and-vite.md","GSAP Animation with Vue 3 and Vite",[7531,8,9],{"type":14,"value":10158,"toc":12290},[10159,10162,10176,10178,10182,10187,10190,10201,10203,10205,10219,10222,10250,10254,10257,10272,10278,10299,10302,10306,10313,10316,10348,10362,10370,10376,10499,10502,10507,10529,10610,10613,10618,10640,10649,10652,10724,10729,10745,10857,10863,10917,10933,10940,10943,10992,11009,11064,11075,11322,11325,11330,11337,11342,11348,11477,11482,11486,11489,11512,11518,11648,11651,12048,12053,12064,12068,12071,12095,12098,12110,12138,12140,12148,12174,12177,12241,12249,12251,12287],[17,10160,10155],{"id":10161},"gsap-animation-with-vue-3-and-vite",[27,10163,10164],{},[30,10165,10166,36,10168,40,10170],{},[33,10167],{"value":35},[33,10169],{"value":39},[42,10171,10172],{},[45,10173,10174],{"href":47},[33,10175],{"value":50},[52,10177],{":tags":54},[56,10179],{":audio-src":10180,":transcript-src":10181},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F04\u002F14\u002Fgsap-animation-with-vue3-and-vite\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F04\u002F14\u002Fgsap-animation-with-vue3-and-vite\u002Fsummary.json",[27,10183,10184],{},[63,10185],{"alt":65,"src":10186},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1649846664\u002Fblog\u002Fgsap-animation-with-vue3-and-vite\u002Fgsap-animation-with-vue3-and-vite",[27,10188,10189],{},"GreenSock Animation Platform (GSAP) is the most robust JavaScript animation library to date that allows developers to animate literally any DOM element with a breeze. GSAP provides an API that can be used for complex animation to be created. Hence, is still supported by all major browsers. In comparison to CSS animation, sequencing in GSAP is very easy. So let's have a look at GSAP. We are going to use GSAP in a Vue 3 project. We will use Vite as a build tool.",[3244,10191,10192],{},[27,10193,10194,10195,10200],{},"Note: You need to have Node.js installed on your machine before following the example below. Please refer to the Node.js ",[45,10196,10199],{"href":10197,"target":2716,"rel":10198},"https:\u002F\u002Fnodejs.org\u002Fen\u002F",[2718,2719],"website"," for more information on how you can install Node.js on your machine.",[104,10202,9523],{"id":9522},[27,10204,9526],{},[128,10206,10207],{"className":8665,"code":9529,"language":8667,"meta":133,"style":133},[22,10208,10209],{"__ignoreMap":133},[137,10210,10211,10213,10215,10217],{"class":139,"line":140},[137,10212,9536],{"class":147},[137,10214,9539],{"class":284},[137,10216,9542],{"class":284},[137,10218,9545],{"class":284},[27,10220,10221],{},"We next follow the prompts and choose Vue with JavaScript. Lastly, install the dependency and run the project locally:",[128,10223,10224],{"className":8665,"code":9551,"language":8667,"meta":133,"style":133},[22,10225,10226,10232,10236,10242],{"__ignoreMap":133},[137,10227,10228,10230],{"class":139,"line":140},[137,10229,9558],{"class":364},[137,10231,9545],{"class":284},[137,10233,10234],{"class":139,"line":173},[137,10235,516],{"emptyLinePlaceholder":515},[137,10237,10238,10240],{"class":139,"line":188},[137,10239,9536],{"class":147},[137,10241,9571],{"class":284},[137,10243,10244,10246,10248],{"class":139,"line":269},[137,10245,9536],{"class":147},[137,10247,9578],{"class":284},[137,10249,9581],{"class":284},[104,10251,10253],{"id":10252},"install-gsap","Install GSAP",[27,10255,10256],{},"Install GSAP before using GSAP in your project. We can install GSAP with the following command:",[128,10258,10260],{"className":8665,"code":10259,"language":8667,"meta":133,"style":133},"npm install gsap\n",[22,10261,10262],{"__ignoreMap":133},[137,10263,10264,10266,10269],{"class":139,"line":140},[137,10265,9536],{"class":147},[137,10267,10268],{"class":284}," install",[137,10270,10271],{"class":284}," gsap\n",[27,10273,10274,10275,10277],{},"Next, import it to our Vue app. We can do that by simply importing the library in the ",[42,10276,9736],{}," file.",[128,10279,10281],{"className":8665,"code":10280,"language":8667,"meta":133,"style":133},"import gsap from \"gsap\";\n",[22,10282,10283],{"__ignoreMap":133},[137,10284,10285,10288,10291,10294,10297],{"class":139,"line":140},[137,10286,10287],{"class":147},"import",[137,10289,10290],{"class":284}," gsap",[137,10292,10293],{"class":284}," from",[137,10295,10296],{"class":284}," \"gsap\"",[137,10298,3276],{"class":157},[27,10300,10301],{},"That's it! We can now use GSAP in our Vue app.",[104,10303,10305],{"id":10304},"getting-started-with-gsap-in-vue","Getting Started with GSAP in Vue",[27,10307,10308,10309,1017],{},"In this blog article, we are going to animate DOM elements. GSAP is also capable of animating SVG, Canvas, WebGL, JS object etc. The full documentation of GSAP can be found at this ",[45,10310,2726],{"href":10311,"target":2716,"rel":10312},"https:\u002F\u002Fgreensock.com\u002Fdocs\u002Fv3\u002FGSAP",[2718,2719],[27,10314,10315],{},"Let's start by animating a DOM element from one state to another. We can do that with the to method in GSAP. For example that would be:",[128,10317,10319],{"className":130,"code":10318,"language":132,"meta":133,"style":133},"gsap.to(\".box\", { x: 300, duration: 2 });\n",[22,10320,10321],{"__ignoreMap":133},[137,10322,10323,10326,10329,10331,10334,10337,10340,10343,10346],{"class":139,"line":140},[137,10324,10325],{"class":157},"gsap.",[137,10327,10328],{"class":147},"to",[137,10330,356],{"class":157},[137,10332,10333],{"class":284},"\".box\"",[137,10335,10336],{"class":157},", { x: ",[137,10338,10339],{"class":364},"300",[137,10341,10342],{"class":157},", duration: ",[137,10344,10345],{"class":364},"2",[137,10347,4168],{"class":157},[27,10349,10350,10351,10354,10355,10357,10358,1017],{},"The first parameter is a target object. The target can be Selector text, Variable, Object or even an Array. We can technically pass a CSS class or an ID as a string and GSAP can magically find that DOM element. But since we use Vue, it is best practice to use template ",[22,10352,10353],{},"refs"," for that purpose. For more about how template ",[22,10356,10353],{}," in Vue work please refer to this ",[45,10359,2726],{"href":10360,"target":2716,"rel":10361},"https:\u002F\u002Fvuejs.org\u002Fguide\u002Fessentials\u002Ftemplate-refs.html",[2718,2719],[27,10363,10364,10365,3955,10368,1017],{},"The second parameter is configuring objects with destination properties that we want to animate along with some special properties such as ",[22,10366,10367],{},"duration",[22,10369,2147],{},[27,10371,10372,10373,10375],{},"Let's see an example inside our Vue 3 project. In the ",[42,10374,9736],{}," file we are going place the following code:",[128,10377,10379],{"className":130,"code":10378,"language":132,"meta":133,"style":133},"\u003Cscript setup>\nimport { ref, onMounted } from \"vue\";\nimport gsap from \"gsap\";\n\nconst logo = ref(null);\n\nonMounted(() => {\n    gsap.to(logo.value, { x: 300, duration: 2 });\n});\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cimg ref=\"logo\" src=\".\u002Fassets\u002Flogo.png\" alt=\"Logo\" \u002F>\n\u003C\u002Ftemplate>\n",[22,10380,10381,10391,10396,10400,10404,10409,10413,10418,10436,10440,10448,10452,10460,10491],{"__ignoreMap":133},[137,10382,10383,10385,10387,10389],{"class":139,"line":140},[137,10384,4033],{"class":157},[137,10386,4037],{"class":4036},[137,10388,9642],{"class":147},[137,10390,4053],{"class":157},[137,10392,10393],{"class":139,"line":173},[137,10394,10395],{"class":157},"import { ref, onMounted } from \"vue\";\n",[137,10397,10398],{"class":139,"line":188},[137,10399,10280],{"class":157},[137,10401,10402],{"class":139,"line":269},[137,10403,516],{"emptyLinePlaceholder":515},[137,10405,10406],{"class":139,"line":278},[137,10407,10408],{"class":157},"const logo = ref(null);\n",[137,10410,10411],{"class":139,"line":291},[137,10412,516],{"emptyLinePlaceholder":515},[137,10414,10415],{"class":139,"line":297},[137,10416,10417],{"class":157},"onMounted(() => {\n",[137,10419,10420,10423,10425,10428,10430,10432,10434],{"class":139,"line":302},[137,10421,10422],{"class":157},"    gsap.",[137,10424,10328],{"class":147},[137,10426,10427],{"class":157},"(logo.value, { x: ",[137,10429,10339],{"class":364},[137,10431,10342],{"class":157},[137,10433,10345],{"class":364},[137,10435,4168],{"class":157},[137,10437,10438],{"class":139,"line":662},[137,10439,5422],{"class":157},[137,10441,10442,10444,10446],{"class":139,"line":667},[137,10443,4083],{"class":157},[137,10445,4037],{"class":4036},[137,10447,4053],{"class":157},[137,10449,10450],{"class":139,"line":786},[137,10451,516],{"emptyLinePlaceholder":515},[137,10453,10454,10456,10458],{"class":139,"line":798},[137,10455,4033],{"class":157},[137,10457,7821],{"class":4036},[137,10459,4053],{"class":157},[137,10461,10462,10464,10466,10469,10471,10474,10476,10478,10481,10484,10486,10489],{"class":139,"line":803},[137,10463,4072],{"class":157},[137,10465,63],{"class":4036},[137,10467,10468],{"class":147}," ref",[137,10470,253],{"class":143},[137,10472,10473],{"class":284},"\"logo\"",[137,10475,4040],{"class":147},[137,10477,253],{"class":143},[137,10479,10480],{"class":284},"\".\u002Fassets\u002Flogo.png\"",[137,10482,10483],{"class":147}," alt",[137,10485,253],{"class":143},[137,10487,10488],{"class":284},"\"Logo\"",[137,10490,4078],{"class":157},[137,10492,10493,10495,10497],{"class":139,"line":931},[137,10494,4083],{"class":157},[137,10496,7821],{"class":4036},[137,10498,4053],{"class":157},[27,10500,10501],{},"If we run our application, we will observe the logo moving to the right by 300px for a duration of 2 seconds. See the animation below:",[27,10503,10504],{},[63,10505],{"alt":65,"src":10506},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1649848120\u002Fblog\u002Fgsap-animation-with-vue3-and-vite\u002Fgetting-started-with-gsap-in-vue",[27,10508,10509,10510,10513,10514,10516,10517,10520,10521,10524,10525,10528],{},"Now let's animate more properties. For example, we can animate a ",[22,10511,10512],{},"background-color",". Since GSAP is a JavaScript library, we have to use camelcase syntax instead of the dash so that the",[22,10515,10512],{}," will become ",[22,10518,10519],{},"backgroundColor",". We can also animate the ",[22,10522,10523],{},"borderRadius"," and the ",[22,10526,10527],{},"border"," properties:",[128,10530,10532],{"className":130,"code":10531,"language":132,"meta":133,"style":133},"onMounted(() => {\n    gsap.to(logo.value, {\n        x: 300,\n        backgroundColor: \"#f0c690\",\n        borderRadius: \"20%\",\n        border: \"5px solid grey\",\n        duration: 2,\n    });\n});\n",[22,10533,10534,10545,10554,10563,10573,10583,10593,10602,10606],{"__ignoreMap":133},[137,10535,10536,10539,10541,10543],{"class":139,"line":140},[137,10537,10538],{"class":147},"onMounted",[137,10540,3193],{"class":157},[137,10542,222],{"class":143},[137,10544,256],{"class":157},[137,10546,10547,10549,10551],{"class":139,"line":173},[137,10548,10422],{"class":157},[137,10550,10328],{"class":147},[137,10552,10553],{"class":157},"(logo.value, {\n",[137,10555,10556,10559,10561],{"class":139,"line":188},[137,10557,10558],{"class":157},"        x: ",[137,10560,10339],{"class":364},[137,10562,1961],{"class":157},[137,10564,10565,10568,10571],{"class":139,"line":269},[137,10566,10567],{"class":157},"        backgroundColor: ",[137,10569,10570],{"class":284},"\"#f0c690\"",[137,10572,1961],{"class":157},[137,10574,10575,10578,10581],{"class":139,"line":278},[137,10576,10577],{"class":157},"        borderRadius: ",[137,10579,10580],{"class":284},"\"20%\"",[137,10582,1961],{"class":157},[137,10584,10585,10588,10591],{"class":139,"line":291},[137,10586,10587],{"class":157},"        border: ",[137,10589,10590],{"class":284},"\"5px solid grey\"",[137,10592,1961],{"class":157},[137,10594,10595,10598,10600],{"class":139,"line":297},[137,10596,10597],{"class":157},"        duration: ",[137,10599,10345],{"class":364},[137,10601,1961],{"class":157},[137,10603,10604],{"class":139,"line":302},[137,10605,2832],{"class":157},[137,10607,10608],{"class":139,"line":662},[137,10609,5422],{"class":157},[27,10611,10612],{},"If we run our app, we should now observe GSAP animating all values at the same time. Refer to the animation below:",[27,10614,10615],{},[63,10616],{"alt":65,"src":10617},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1649848297\u002Fblog\u002Fgsap-animation-with-vue3-and-vite\u002Fgetting-started-with-gsap-in-vue-aphtg9",[27,10619,10620,10621,3955,10623,10625,10626,10629,10630,164,10633,164,10636,10639],{},"As we mentioned earlier, configuration objects have special properties such as ",[22,10622,10367],{},[22,10624,2147],{},". Another useful special property is the ",[22,10627,10628],{},"ease","property. In GSAP there are many built-in easing options to choose from such as: ",[22,10631,10632],{},"back",[22,10634,10635],{},"elastic",[22,10637,10638],{},"bounce"," etc.",[27,10641,10642,10643,10648],{},"For a full list of easing options in GSAP please refer to the documentation ",[45,10644,10647],{"href":10645,"target":2716,"rel":10646},"https:\u002F\u002Fgreensock.com\u002Fdocs\u002Fv3\u002FEases",[2718,2719],"here",". GSAP also has a handy Visualiser to explore all the easing options available.",[27,10650,10651],{},"Here is the code to apply easing to our animation:",[128,10653,10655],{"className":130,"code":10654,"language":132,"meta":133,"style":133},"gsap.to(logo.value, {\n    x: 300,\n    backgroundColor: \"#f0c690\",\n    borderRadius: \"20%\",\n    border: \"5px solid grey\",\n    duration: 2,\n    ease: \"elastic\",\n});\n",[22,10656,10657,10665,10674,10683,10692,10701,10710,10720],{"__ignoreMap":133},[137,10658,10659,10661,10663],{"class":139,"line":140},[137,10660,10325],{"class":157},[137,10662,10328],{"class":147},[137,10664,10553],{"class":157},[137,10666,10667,10670,10672],{"class":139,"line":173},[137,10668,10669],{"class":157},"    x: ",[137,10671,10339],{"class":364},[137,10673,1961],{"class":157},[137,10675,10676,10679,10681],{"class":139,"line":188},[137,10677,10678],{"class":157},"    backgroundColor: ",[137,10680,10570],{"class":284},[137,10682,1961],{"class":157},[137,10684,10685,10688,10690],{"class":139,"line":269},[137,10686,10687],{"class":157},"    borderRadius: ",[137,10689,10580],{"class":284},[137,10691,1961],{"class":157},[137,10693,10694,10697,10699],{"class":139,"line":278},[137,10695,10696],{"class":157},"    border: ",[137,10698,10590],{"class":284},[137,10700,1961],{"class":157},[137,10702,10703,10706,10708],{"class":139,"line":291},[137,10704,10705],{"class":157},"    duration: ",[137,10707,10345],{"class":364},[137,10709,1961],{"class":157},[137,10711,10712,10715,10718],{"class":139,"line":297},[137,10713,10714],{"class":157},"    ease: ",[137,10716,10717],{"class":284},"\"elastic\"",[137,10719,1961],{"class":157},[137,10721,10722],{"class":139,"line":302},[137,10723,5422],{"class":157},[27,10725,10726],{},[63,10727],{"alt":65,"src":10728},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1649848437\u002Fblog\u002Fgsap-animation-with-vue3-and-vite\u002Fgetting-started-with-gsap-in-vue-dk8mt9",[27,10730,10731,10732,164,10735,164,10738,10741,10742,10744],{},"GSAP also has a few handy callback functions: ",[22,10733,10734],{},"onStart",[22,10736,10737],{},"onUpdate",[22,10739,10740],{},"onComplete"," etc. For example, if we want to listen in on style updates we can use ",[22,10743,10737],{}," and then pass a callback function. Please see the example below:",[128,10746,10748],{"className":130,"code":10747,"language":132,"meta":133,"style":133},"onMounted(() => {\n    gsap.to(logo.value, {\n        x: 300,\n        backgroundColor: \"#f0c690\",\n        borderRadius: \"20%\",\n        border: \"5px solid grey\",\n        duration: 2,\n        ease: \"bounce\",\n        onUpdate: getBorderRadious,\n    });\n});\n\nfunction getBorderRadious() {\n    console.log(logo.value.style.borderRadius);\n}\n",[22,10749,10750,10760,10768,10776,10784,10792,10800,10808,10818,10823,10827,10831,10835,10844,10853],{"__ignoreMap":133},[137,10751,10752,10754,10756,10758],{"class":139,"line":140},[137,10753,10538],{"class":147},[137,10755,3193],{"class":157},[137,10757,222],{"class":143},[137,10759,256],{"class":157},[137,10761,10762,10764,10766],{"class":139,"line":173},[137,10763,10422],{"class":157},[137,10765,10328],{"class":147},[137,10767,10553],{"class":157},[137,10769,10770,10772,10774],{"class":139,"line":188},[137,10771,10558],{"class":157},[137,10773,10339],{"class":364},[137,10775,1961],{"class":157},[137,10777,10778,10780,10782],{"class":139,"line":269},[137,10779,10567],{"class":157},[137,10781,10570],{"class":284},[137,10783,1961],{"class":157},[137,10785,10786,10788,10790],{"class":139,"line":278},[137,10787,10577],{"class":157},[137,10789,10580],{"class":284},[137,10791,1961],{"class":157},[137,10793,10794,10796,10798],{"class":139,"line":291},[137,10795,10587],{"class":157},[137,10797,10590],{"class":284},[137,10799,1961],{"class":157},[137,10801,10802,10804,10806],{"class":139,"line":297},[137,10803,10597],{"class":157},[137,10805,10345],{"class":364},[137,10807,1961],{"class":157},[137,10809,10810,10813,10816],{"class":139,"line":302},[137,10811,10812],{"class":157},"        ease: ",[137,10814,10815],{"class":284},"\"bounce\"",[137,10817,1961],{"class":157},[137,10819,10820],{"class":139,"line":662},[137,10821,10822],{"class":157},"        onUpdate: getBorderRadious,\n",[137,10824,10825],{"class":139,"line":667},[137,10826,2832],{"class":157},[137,10828,10829],{"class":139,"line":786},[137,10830,5422],{"class":157},[137,10832,10833],{"class":139,"line":798},[137,10834,516],{"emptyLinePlaceholder":515},[137,10836,10837,10839,10842],{"class":139,"line":803},[137,10838,483],{"class":143},[137,10840,10841],{"class":147}," getBorderRadious",[137,10843,275],{"class":157},[137,10845,10846,10848,10850],{"class":139,"line":931},[137,10847,493],{"class":157},[137,10849,353],{"class":147},[137,10851,10852],{"class":157},"(logo.value.style.borderRadius);\n",[137,10854,10855],{"class":139,"line":1568},[137,10856,510],{"class":157},[27,10858,10859,10860,10862],{},"We output every change of the ",[22,10861,10523],{}," property in the console:",[128,10864,10866],{"className":8665,"code":10865,"language":8667,"meta":133,"style":133},"...\n...\n...\n8.7483%\n9.3777%\n10.0288%\n10.6617%\n11.3552%\n...\n...\n...\n",[22,10867,10868,10872,10876,10880,10885,10890,10895,10900,10905,10909,10913],{"__ignoreMap":133},[137,10869,10870],{"class":139,"line":140},[137,10871,4058],{"class":364},[137,10873,10874],{"class":139,"line":173},[137,10875,4058],{"class":364},[137,10877,10878],{"class":139,"line":188},[137,10879,4058],{"class":364},[137,10881,10882],{"class":139,"line":269},[137,10883,10884],{"class":147},"8.7483%\n",[137,10886,10887],{"class":139,"line":278},[137,10888,10889],{"class":147},"9.3777%\n",[137,10891,10892],{"class":139,"line":291},[137,10893,10894],{"class":147},"10.0288%\n",[137,10896,10897],{"class":139,"line":297},[137,10898,10899],{"class":147},"10.6617%\n",[137,10901,10902],{"class":139,"line":302},[137,10903,10904],{"class":147},"11.3552%\n",[137,10906,10907],{"class":139,"line":662},[137,10908,4058],{"class":364},[137,10910,10911],{"class":139,"line":667},[137,10912,4058],{"class":364},[137,10914,10915],{"class":139,"line":786},[137,10916,4058],{"class":364},[27,10918,10919,10920,10928,10929,10932],{},"We have just shown how to animate to a certain state or value in the example above. What if we would now like to animate from those values? It will be as simple as changing ",[45,10921,10925],{"href":10922,"rel":10923},"http:\u002F\u002Fgsap.to",[10924],"nofollow",[22,10926,10927],{},"gsap.to"," to ",[22,10930,10931],{},"gsap.from"," .",[104,10934,10936,10939],{"id":10935},"stagger-special-property",[22,10937,10938],{},"stagger"," special property",[27,10941,10942],{},"We can animate all elements with the same selector at once. Let's view the example below:",[128,10944,10946],{"className":130,"code":10945,"language":132,"meta":133,"style":133},"gsap.from(\".circle\", {\n    y: 300,\n    opacity: 0,\n    duration: 2,\n});\n",[22,10947,10948,10962,10971,10980,10988],{"__ignoreMap":133},[137,10949,10950,10952,10955,10957,10960],{"class":139,"line":140},[137,10951,10325],{"class":157},[137,10953,10954],{"class":147},"from",[137,10956,356],{"class":157},[137,10958,10959],{"class":284},"\".circle\"",[137,10961,5396],{"class":157},[137,10963,10964,10967,10969],{"class":139,"line":173},[137,10965,10966],{"class":157},"    y: ",[137,10968,10339],{"class":364},[137,10970,1961],{"class":157},[137,10972,10973,10976,10978],{"class":139,"line":188},[137,10974,10975],{"class":157},"    opacity: ",[137,10977,6044],{"class":364},[137,10979,1961],{"class":157},[137,10981,10982,10984,10986],{"class":139,"line":269},[137,10983,10705],{"class":157},[137,10985,10345],{"class":364},[137,10987,1961],{"class":157},[137,10989,10990],{"class":139,"line":278},[137,10991,5422],{"class":157},[27,10993,10994,10995,10998,10999,11001,11002,11004,11005,11008],{},"If we run our app, we observe that all elements with a class of ",[22,10996,10997],{},".circle"," will be animated at the same time. By animating all elements together, the dynamism of the animation is lost. To counter this, GSAP has a special property called ",[22,11000,10938],{}," that allows us to animate elements with an offset time. We only need to pass an offset time value to the ",[22,11003,10938],{}," for example ",[22,11006,11007],{},"0.25",". In this case, the elements will be animated within offset of a quarter of a second from the previous one.",[128,11010,11012],{"className":130,"code":11011,"language":132,"meta":133,"style":133},"gsap.from(\".circle\", {\n    y: 300,\n    opacity: 0,\n    duration: 2,\n    stagger: 0.15,\n});\n",[22,11013,11014,11026,11034,11042,11050,11060],{"__ignoreMap":133},[137,11015,11016,11018,11020,11022,11024],{"class":139,"line":140},[137,11017,10325],{"class":157},[137,11019,10954],{"class":147},[137,11021,356],{"class":157},[137,11023,10959],{"class":284},[137,11025,5396],{"class":157},[137,11027,11028,11030,11032],{"class":139,"line":173},[137,11029,10966],{"class":157},[137,11031,10339],{"class":364},[137,11033,1961],{"class":157},[137,11035,11036,11038,11040],{"class":139,"line":188},[137,11037,10975],{"class":157},[137,11039,6044],{"class":364},[137,11041,1961],{"class":157},[137,11043,11044,11046,11048],{"class":139,"line":269},[137,11045,10705],{"class":157},[137,11047,10345],{"class":364},[137,11049,1961],{"class":157},[137,11051,11052,11055,11058],{"class":139,"line":278},[137,11053,11054],{"class":157},"    stagger: ",[137,11056,11057],{"class":364},"0.15",[137,11059,1961],{"class":157},[137,11061,11062],{"class":139,"line":291},[137,11063,5422],{"class":157},[27,11065,11066,11067,11070,11071,11074],{},"GSAP also provides built-in functionalities for the generation of random values. This is very handy if we want random ",[22,11068,11069],{},"y"," values to be animated every time we run it. We can do that by passing ",[22,11072,11073],{},"\"random(-200, 200)\""," where the range of the random value generated is set between -200 to 200.",[128,11076,11078],{"className":130,"code":11077,"language":132,"meta":133,"style":133},"import { onMounted } from \"vue\";\nimport gsap from \"gsap\";\n\nonMounted(() => {\n    gsap.from(\".circle\", {\n        y: \"random(-200, 200)\",\n        opacity: 0,\n        duration: 1,\n        stagger: 0.15,\n        delay: 2,\n    });\n});\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv class=\"circle\">\u003C\u002Fdiv>\n    \u003Cdiv class=\"circle\">\u003C\u002Fdiv>\n    \u003Cdiv class=\"circle\">\u003C\u002Fdiv>\n    \u003Cdiv class=\"circle\">\u003C\u002Fdiv>\n    \u003Cdiv class=\"circle\">\u003C\u002Fdiv>\n    \u003Cdiv class=\"circle\">\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,11079,11080,11094,11107,11111,11121,11133,11142,11151,11159,11168,11177,11181,11185,11193,11197,11205,11224,11242,11260,11278,11296,11314],{"__ignoreMap":133},[137,11081,11082,11084,11087,11089,11092],{"class":139,"line":140},[137,11083,10287],{"class":143},[137,11085,11086],{"class":157}," { onMounted } ",[137,11088,10954],{"class":143},[137,11090,11091],{"class":284}," \"vue\"",[137,11093,3276],{"class":157},[137,11095,11096,11098,11101,11103,11105],{"class":139,"line":173},[137,11097,10287],{"class":143},[137,11099,11100],{"class":157}," gsap ",[137,11102,10954],{"class":143},[137,11104,10296],{"class":284},[137,11106,3276],{"class":157},[137,11108,11109],{"class":139,"line":188},[137,11110,516],{"emptyLinePlaceholder":515},[137,11112,11113,11115,11117,11119],{"class":139,"line":269},[137,11114,10538],{"class":147},[137,11116,3193],{"class":157},[137,11118,222],{"class":143},[137,11120,256],{"class":157},[137,11122,11123,11125,11127,11129,11131],{"class":139,"line":278},[137,11124,10422],{"class":157},[137,11126,10954],{"class":147},[137,11128,356],{"class":157},[137,11130,10959],{"class":284},[137,11132,5396],{"class":157},[137,11134,11135,11138,11140],{"class":139,"line":291},[137,11136,11137],{"class":157},"        y: ",[137,11139,11073],{"class":284},[137,11141,1961],{"class":157},[137,11143,11144,11147,11149],{"class":139,"line":297},[137,11145,11146],{"class":157},"        opacity: ",[137,11148,6044],{"class":364},[137,11150,1961],{"class":157},[137,11152,11153,11155,11157],{"class":139,"line":302},[137,11154,10597],{"class":157},[137,11156,6065],{"class":364},[137,11158,1961],{"class":157},[137,11160,11161,11164,11166],{"class":139,"line":662},[137,11162,11163],{"class":157},"        stagger: ",[137,11165,11057],{"class":364},[137,11167,1961],{"class":157},[137,11169,11170,11173,11175],{"class":139,"line":667},[137,11171,11172],{"class":157},"        delay: ",[137,11174,10345],{"class":364},[137,11176,1961],{"class":157},[137,11178,11179],{"class":139,"line":786},[137,11180,2832],{"class":157},[137,11182,11183],{"class":139,"line":798},[137,11184,5422],{"class":157},[137,11186,11187,11189,11191],{"class":139,"line":803},[137,11188,4083],{"class":143},[137,11190,4037],{"class":157},[137,11192,4053],{"class":143},[137,11194,11195],{"class":139,"line":931},[137,11196,516],{"emptyLinePlaceholder":515},[137,11198,11199,11201,11203],{"class":139,"line":1568},[137,11200,4033],{"class":157},[137,11202,7821],{"class":4036},[137,11204,4053],{"class":157},[137,11206,11207,11209,11211,11213,11215,11218,11220,11222],{"class":139,"line":1573},[137,11208,4072],{"class":157},[137,11210,8330],{"class":4036},[137,11212,7832],{"class":147},[137,11214,253],{"class":143},[137,11216,11217],{"class":284},"\"circle\"",[137,11219,4048],{"class":157},[137,11221,8330],{"class":4036},[137,11223,4053],{"class":157},[137,11225,11226,11228,11230,11232,11234,11236,11238,11240],{"class":139,"line":1578},[137,11227,4072],{"class":157},[137,11229,8330],{"class":4036},[137,11231,7832],{"class":147},[137,11233,253],{"class":143},[137,11235,11217],{"class":284},[137,11237,4048],{"class":157},[137,11239,8330],{"class":4036},[137,11241,4053],{"class":157},[137,11243,11244,11246,11248,11250,11252,11254,11256,11258],{"class":139,"line":1588},[137,11245,4072],{"class":157},[137,11247,8330],{"class":4036},[137,11249,7832],{"class":147},[137,11251,253],{"class":143},[137,11253,11217],{"class":284},[137,11255,4048],{"class":157},[137,11257,8330],{"class":4036},[137,11259,4053],{"class":157},[137,11261,11262,11264,11266,11268,11270,11272,11274,11276],{"class":139,"line":1601},[137,11263,4072],{"class":157},[137,11265,8330],{"class":4036},[137,11267,7832],{"class":147},[137,11269,253],{"class":143},[137,11271,11217],{"class":284},[137,11273,4048],{"class":157},[137,11275,8330],{"class":4036},[137,11277,4053],{"class":157},[137,11279,11280,11282,11284,11286,11288,11290,11292,11294],{"class":139,"line":3802},[137,11281,4072],{"class":157},[137,11283,8330],{"class":4036},[137,11285,7832],{"class":147},[137,11287,253],{"class":143},[137,11289,11217],{"class":284},[137,11291,4048],{"class":157},[137,11293,8330],{"class":4036},[137,11295,4053],{"class":157},[137,11297,11298,11300,11302,11304,11306,11308,11310,11312],{"class":139,"line":3808},[137,11299,4072],{"class":157},[137,11301,8330],{"class":4036},[137,11303,7832],{"class":147},[137,11305,253],{"class":143},[137,11307,11217],{"class":284},[137,11309,4048],{"class":157},[137,11311,8330],{"class":4036},[137,11313,4053],{"class":157},[137,11315,11316,11318,11320],{"class":139,"line":3822},[137,11317,4083],{"class":157},[137,11319,7821],{"class":4036},[137,11321,4053],{"class":157},[27,11323,11324],{},"Let's have a look at how the animation would now look like:",[27,11326,11327],{},[63,11328],{"alt":65,"src":11329},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1649848645\u002Fblog\u002Fgsap-animation-with-vue3-and-vite\u002Fgetting-started-with-gsap-in-vue-bo1gml",[27,11331,11332,11333,1017],{},"For more utility functions like the one above, please check out the GSAP full documentation in the following ",[45,11334,2726],{"href":11335,"target":2716,"rel":11336},"https:\u002F\u002Fgreensock.com\u002Fdocs\u002Fv3\u002FGSAP\u002FUtilityMethods",[2718,2719],[104,11338,11340,10939],{"id":11339},"delay-special-property",[22,11341,2147],{},[27,11343,11344,11345,11347],{},"By default both of the logo and the circles will start animating at the same time. If we want the circles to animate after the logo is done with animating, we can use GSAP special ",[22,11346,2147],{}," property, by assigning a value of 2 to the duration of the logo animation. The circle animation will now wait for 2 seconds before it starts animating.",[128,11349,11351],{"className":130,"code":11350,"language":132,"meta":133,"style":133},"gsap.from(logo.value, {\n    x: -300,\n    backgroundColor: \"#f0c690\",\n    borderRadius: \"20%\",\n    border: \"5px solid grey\",\n    duration: 2,\n    ease: \"bounce\",\n    onUpdate: getBorderRadious,\n});\ngsap.from(\".circle\", {\n    y: \"random(-200, 200)\",\n    opacity: 0,\n    duration: 1,\n    stagger: 0.15,\n    delay: 2,\n});\n",[22,11352,11353,11361,11371,11379,11387,11395,11403,11411,11416,11420,11432,11440,11448,11456,11464,11473],{"__ignoreMap":133},[137,11354,11355,11357,11359],{"class":139,"line":140},[137,11356,10325],{"class":157},[137,11358,10954],{"class":147},[137,11360,10553],{"class":157},[137,11362,11363,11365,11367,11369],{"class":139,"line":173},[137,11364,10669],{"class":157},[137,11366,8215],{"class":143},[137,11368,10339],{"class":364},[137,11370,1961],{"class":157},[137,11372,11373,11375,11377],{"class":139,"line":188},[137,11374,10678],{"class":157},[137,11376,10570],{"class":284},[137,11378,1961],{"class":157},[137,11380,11381,11383,11385],{"class":139,"line":269},[137,11382,10687],{"class":157},[137,11384,10580],{"class":284},[137,11386,1961],{"class":157},[137,11388,11389,11391,11393],{"class":139,"line":278},[137,11390,10696],{"class":157},[137,11392,10590],{"class":284},[137,11394,1961],{"class":157},[137,11396,11397,11399,11401],{"class":139,"line":291},[137,11398,10705],{"class":157},[137,11400,10345],{"class":364},[137,11402,1961],{"class":157},[137,11404,11405,11407,11409],{"class":139,"line":297},[137,11406,10714],{"class":157},[137,11408,10815],{"class":284},[137,11410,1961],{"class":157},[137,11412,11413],{"class":139,"line":302},[137,11414,11415],{"class":157},"    onUpdate: getBorderRadious,\n",[137,11417,11418],{"class":139,"line":662},[137,11419,5422],{"class":157},[137,11421,11422,11424,11426,11428,11430],{"class":139,"line":667},[137,11423,10325],{"class":157},[137,11425,10954],{"class":147},[137,11427,356],{"class":157},[137,11429,10959],{"class":284},[137,11431,5396],{"class":157},[137,11433,11434,11436,11438],{"class":139,"line":786},[137,11435,10966],{"class":157},[137,11437,11073],{"class":284},[137,11439,1961],{"class":157},[137,11441,11442,11444,11446],{"class":139,"line":798},[137,11443,10975],{"class":157},[137,11445,6044],{"class":364},[137,11447,1961],{"class":157},[137,11449,11450,11452,11454],{"class":139,"line":803},[137,11451,10705],{"class":157},[137,11453,6065],{"class":364},[137,11455,1961],{"class":157},[137,11457,11458,11460,11462],{"class":139,"line":931},[137,11459,11054],{"class":157},[137,11461,11057],{"class":364},[137,11463,1961],{"class":157},[137,11465,11466,11469,11471],{"class":139,"line":1568},[137,11467,11468],{"class":157},"    delay: ",[137,11470,10345],{"class":364},[137,11472,1961],{"class":157},[137,11474,11475],{"class":139,"line":1573},[137,11476,5422],{"class":157},[27,11478,4737,11479,11481],{},[22,11480,2147],{}," property is handy for simple animations. But for complex animations, GSAP has the timeline feature that allows for that.",[104,11483,11485],{"id":11484},"gsap-timeline","GSAP Timeline",[27,11487,11488],{},"The timeline feature in GSAP allows us to build complex animations simpler. Let's create our first timeline.",[128,11490,11492],{"className":130,"code":11491,"language":132,"meta":133,"style":133},"let tl = gsap.timeline();\n",[22,11493,11494],{"__ignoreMap":133},[137,11495,11496,11499,11502,11504,11507,11510],{"class":139,"line":140},[137,11497,11498],{"class":143},"let",[137,11500,11501],{"class":157}," tl ",[137,11503,253],{"class":143},[137,11505,11506],{"class":157}," gsap.",[137,11508,11509],{"class":147},"timeline",[137,11511,924],{"class":157},[27,11513,11514,11515,11517],{},"We don't need to use the ",[22,11516,2147],{}," property for each animation. Instead, we can use timelines. By default, Timeslines adds animations at the end of timeline so we get perfect sequencing right from the box.",[128,11519,11521],{"className":130,"code":11520,"language":132,"meta":133,"style":133},"let tl = gsap.timeline();\ntl.from(logo.value, {\n    x: -300,\n    backgroundColor: \"#f0c690\",\n    borderRadius: \"20%\",\n    border: \"5px solid grey\",\n    duration: 2,\n    ease: \"bounce\",\n});\ntl.from(\".circle\", {\n    y: \"random(-200, 200)\",\n    opacity: 0,\n    duration: 1,\n    stagger: 0.15,\n});\n",[22,11522,11523,11537,11546,11556,11564,11572,11580,11588,11596,11600,11612,11620,11628,11636,11644],{"__ignoreMap":133},[137,11524,11525,11527,11529,11531,11533,11535],{"class":139,"line":140},[137,11526,11498],{"class":143},[137,11528,11501],{"class":157},[137,11530,253],{"class":143},[137,11532,11506],{"class":157},[137,11534,11509],{"class":147},[137,11536,924],{"class":157},[137,11538,11539,11542,11544],{"class":139,"line":173},[137,11540,11541],{"class":157},"tl.",[137,11543,10954],{"class":147},[137,11545,10553],{"class":157},[137,11547,11548,11550,11552,11554],{"class":139,"line":188},[137,11549,10669],{"class":157},[137,11551,8215],{"class":143},[137,11553,10339],{"class":364},[137,11555,1961],{"class":157},[137,11557,11558,11560,11562],{"class":139,"line":269},[137,11559,10678],{"class":157},[137,11561,10570],{"class":284},[137,11563,1961],{"class":157},[137,11565,11566,11568,11570],{"class":139,"line":278},[137,11567,10687],{"class":157},[137,11569,10580],{"class":284},[137,11571,1961],{"class":157},[137,11573,11574,11576,11578],{"class":139,"line":291},[137,11575,10696],{"class":157},[137,11577,10590],{"class":284},[137,11579,1961],{"class":157},[137,11581,11582,11584,11586],{"class":139,"line":297},[137,11583,10705],{"class":157},[137,11585,10345],{"class":364},[137,11587,1961],{"class":157},[137,11589,11590,11592,11594],{"class":139,"line":302},[137,11591,10714],{"class":157},[137,11593,10815],{"class":284},[137,11595,1961],{"class":157},[137,11597,11598],{"class":139,"line":662},[137,11599,5422],{"class":157},[137,11601,11602,11604,11606,11608,11610],{"class":139,"line":667},[137,11603,11541],{"class":157},[137,11605,10954],{"class":147},[137,11607,356],{"class":157},[137,11609,10959],{"class":284},[137,11611,5396],{"class":157},[137,11613,11614,11616,11618],{"class":139,"line":786},[137,11615,10966],{"class":157},[137,11617,11073],{"class":284},[137,11619,1961],{"class":157},[137,11621,11622,11624,11626],{"class":139,"line":798},[137,11623,10975],{"class":157},[137,11625,6044],{"class":364},[137,11627,1961],{"class":157},[137,11629,11630,11632,11634],{"class":139,"line":803},[137,11631,10705],{"class":157},[137,11633,6065],{"class":364},[137,11635,1961],{"class":157},[137,11637,11638,11640,11642],{"class":139,"line":931},[137,11639,11054],{"class":157},[137,11641,11057],{"class":364},[137,11643,1961],{"class":157},[137,11645,11646],{"class":139,"line":1568},[137,11647,5422],{"class":157},[27,11649,11650],{},"If we want to control precisely the position of the animation in the timeline we can use the position parameter. This parameter comes after the configuration object, and tells the timeline where to place the animation. In our example, we are going to pass \"+=1\" value. This means that the animation will start 1 second past the end of the timeline. Hence, creates a gap.",[128,11652,11654],{"className":130,"code":11653,"language":132,"meta":133,"style":133},"import { ref, onMounted } from \"vue\";\nimport gsap from \"gsap\";\n\nconst logo = ref(null);\nlet tl = null;\n\nonMounted(() => {\n    tl = gsap.timeline();\n    tl.from(logo.value, {\n        x: -300,\n        backgroundColor: \"#f0c690\",\n        borderRadius: \"20%\",\n        border: \"5px solid grey\",\n        duration: 2,\n        ease: \"bounce\",\n    });\n    tl.from(\n        \".circle\",\n        {\n            y: \"random(-200, 200)\",\n            opacity: 0,\n            duration: 1,\n            stagger: 0.15,\n        },\n        \"+=1\"\n    );\n});\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cimg ref=\"logo\" src=\".\u002Fassets\u002Flogo.png\" alt=\"Logo\" \u002F>\n    \u003Cdiv class=\"circle circle--blue\">\u003C\u002Fdiv>\n    \u003Cdiv class=\"circle circle--red\">\u003C\u002Fdiv>\n    \u003Cdiv class=\"circle circle--yellow\">\u003C\u002Fdiv>\n    \u003Cdiv class=\"circle circle--green\">\u003C\u002Fdiv>\n    \u003Cdiv class=\"circle circle--purple\">\u003C\u002Fdiv>\n    \u003Cdiv class=\"circle circle--pink\">\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,11655,11656,11669,11681,11685,11703,11715,11719,11729,11742,11751,11761,11769,11777,11785,11793,11801,11805,11814,11821,11826,11835,11844,11853,11862,11866,11871,11876,11880,11888,11892,11900,11926,11945,11964,11983,12002,12021,12040],{"__ignoreMap":133},[137,11657,11658,11660,11663,11665,11667],{"class":139,"line":140},[137,11659,10287],{"class":143},[137,11661,11662],{"class":157}," { ref, onMounted } ",[137,11664,10954],{"class":143},[137,11666,11091],{"class":284},[137,11668,3276],{"class":157},[137,11670,11671,11673,11675,11677,11679],{"class":139,"line":173},[137,11672,10287],{"class":143},[137,11674,11100],{"class":157},[137,11676,10954],{"class":143},[137,11678,10296],{"class":284},[137,11680,3276],{"class":157},[137,11682,11683],{"class":139,"line":188},[137,11684,516],{"emptyLinePlaceholder":515},[137,11686,11687,11689,11692,11694,11696,11698,11701],{"class":139,"line":269},[137,11688,3077],{"class":143},[137,11690,11691],{"class":364}," logo",[137,11693,151],{"class":143},[137,11695,10468],{"class":147},[137,11697,356],{"class":157},[137,11699,11700],{"class":364},"null",[137,11702,1502],{"class":157},[137,11704,11705,11707,11709,11711,11713],{"class":139,"line":278},[137,11706,11498],{"class":143},[137,11708,11501],{"class":157},[137,11710,253],{"class":143},[137,11712,3417],{"class":364},[137,11714,3276],{"class":157},[137,11716,11717],{"class":139,"line":291},[137,11718,516],{"emptyLinePlaceholder":515},[137,11720,11721,11723,11725,11727],{"class":139,"line":297},[137,11722,10538],{"class":147},[137,11724,3193],{"class":157},[137,11726,222],{"class":143},[137,11728,256],{"class":157},[137,11730,11731,11734,11736,11738,11740],{"class":139,"line":302},[137,11732,11733],{"class":157},"    tl ",[137,11735,253],{"class":143},[137,11737,11506],{"class":157},[137,11739,11509],{"class":147},[137,11741,924],{"class":157},[137,11743,11744,11747,11749],{"class":139,"line":662},[137,11745,11746],{"class":157},"    tl.",[137,11748,10954],{"class":147},[137,11750,10553],{"class":157},[137,11752,11753,11755,11757,11759],{"class":139,"line":667},[137,11754,10558],{"class":157},[137,11756,8215],{"class":143},[137,11758,10339],{"class":364},[137,11760,1961],{"class":157},[137,11762,11763,11765,11767],{"class":139,"line":786},[137,11764,10567],{"class":157},[137,11766,10570],{"class":284},[137,11768,1961],{"class":157},[137,11770,11771,11773,11775],{"class":139,"line":798},[137,11772,10577],{"class":157},[137,11774,10580],{"class":284},[137,11776,1961],{"class":157},[137,11778,11779,11781,11783],{"class":139,"line":803},[137,11780,10587],{"class":157},[137,11782,10590],{"class":284},[137,11784,1961],{"class":157},[137,11786,11787,11789,11791],{"class":139,"line":931},[137,11788,10597],{"class":157},[137,11790,10345],{"class":364},[137,11792,1961],{"class":157},[137,11794,11795,11797,11799],{"class":139,"line":1568},[137,11796,10812],{"class":157},[137,11798,10815],{"class":284},[137,11800,1961],{"class":157},[137,11802,11803],{"class":139,"line":1573},[137,11804,2832],{"class":157},[137,11806,11807,11809,11811],{"class":139,"line":1578},[137,11808,11746],{"class":157},[137,11810,10954],{"class":147},[137,11812,11813],{"class":157},"(\n",[137,11815,11816,11819],{"class":139,"line":1588},[137,11817,11818],{"class":284},"        \".circle\"",[137,11820,1961],{"class":157},[137,11822,11823],{"class":139,"line":1601},[137,11824,11825],{"class":157},"        {\n",[137,11827,11828,11831,11833],{"class":139,"line":3802},[137,11829,11830],{"class":157},"            y: ",[137,11832,11073],{"class":284},[137,11834,1961],{"class":157},[137,11836,11837,11840,11842],{"class":139,"line":3808},[137,11838,11839],{"class":157},"            opacity: ",[137,11841,6044],{"class":364},[137,11843,1961],{"class":157},[137,11845,11846,11849,11851],{"class":139,"line":3822},[137,11847,11848],{"class":157},"            duration: ",[137,11850,6065],{"class":364},[137,11852,1961],{"class":157},[137,11854,11855,11858,11860],{"class":139,"line":3827},[137,11856,11857],{"class":157},"            stagger: ",[137,11859,11057],{"class":364},[137,11861,1961],{"class":157},[137,11863,11864],{"class":139,"line":3832},[137,11865,2084],{"class":157},[137,11867,11868],{"class":139,"line":3840},[137,11869,11870],{"class":284},"        \"+=1\"\n",[137,11872,11873],{"class":139,"line":3846},[137,11874,11875],{"class":157},"    );\n",[137,11877,11878],{"class":139,"line":3861},[137,11879,5422],{"class":157},[137,11881,11882,11884,11886],{"class":139,"line":3883},[137,11883,4083],{"class":143},[137,11885,4037],{"class":157},[137,11887,4053],{"class":143},[137,11889,11890],{"class":139,"line":3896},[137,11891,516],{"emptyLinePlaceholder":515},[137,11893,11894,11896,11898],{"class":139,"line":3901},[137,11895,4033],{"class":157},[137,11897,7821],{"class":4036},[137,11899,4053],{"class":157},[137,11901,11902,11904,11906,11908,11910,11912,11914,11916,11918,11920,11922,11924],{"class":139,"line":3906},[137,11903,4072],{"class":157},[137,11905,63],{"class":4036},[137,11907,10468],{"class":147},[137,11909,253],{"class":143},[137,11911,10473],{"class":284},[137,11913,4040],{"class":147},[137,11915,253],{"class":143},[137,11917,10480],{"class":284},[137,11919,10483],{"class":147},[137,11921,253],{"class":143},[137,11923,10488],{"class":284},[137,11925,4078],{"class":157},[137,11927,11928,11930,11932,11934,11936,11939,11941,11943],{"class":139,"line":3911},[137,11929,4072],{"class":157},[137,11931,8330],{"class":4036},[137,11933,7832],{"class":147},[137,11935,253],{"class":143},[137,11937,11938],{"class":284},"\"circle circle--blue\"",[137,11940,4048],{"class":157},[137,11942,8330],{"class":4036},[137,11944,4053],{"class":157},[137,11946,11947,11949,11951,11953,11955,11958,11960,11962],{"class":139,"line":4666},[137,11948,4072],{"class":157},[137,11950,8330],{"class":4036},[137,11952,7832],{"class":147},[137,11954,253],{"class":143},[137,11956,11957],{"class":284},"\"circle circle--red\"",[137,11959,4048],{"class":157},[137,11961,8330],{"class":4036},[137,11963,4053],{"class":157},[137,11965,11966,11968,11970,11972,11974,11977,11979,11981],{"class":139,"line":4672},[137,11967,4072],{"class":157},[137,11969,8330],{"class":4036},[137,11971,7832],{"class":147},[137,11973,253],{"class":143},[137,11975,11976],{"class":284},"\"circle circle--yellow\"",[137,11978,4048],{"class":157},[137,11980,8330],{"class":4036},[137,11982,4053],{"class":157},[137,11984,11985,11987,11989,11991,11993,11996,11998,12000],{"class":139,"line":4680},[137,11986,4072],{"class":157},[137,11988,8330],{"class":4036},[137,11990,7832],{"class":147},[137,11992,253],{"class":143},[137,11994,11995],{"class":284},"\"circle circle--green\"",[137,11997,4048],{"class":157},[137,11999,8330],{"class":4036},[137,12001,4053],{"class":157},[137,12003,12004,12006,12008,12010,12012,12015,12017,12019],{"class":139,"line":4711},[137,12005,4072],{"class":157},[137,12007,8330],{"class":4036},[137,12009,7832],{"class":147},[137,12011,253],{"class":143},[137,12013,12014],{"class":284},"\"circle circle--purple\"",[137,12016,4048],{"class":157},[137,12018,8330],{"class":4036},[137,12020,4053],{"class":157},[137,12022,12023,12025,12027,12029,12031,12034,12036,12038],{"class":139,"line":4716},[137,12024,4072],{"class":157},[137,12026,8330],{"class":4036},[137,12028,7832],{"class":147},[137,12030,253],{"class":143},[137,12032,12033],{"class":284},"\"circle circle--pink\"",[137,12035,4048],{"class":157},[137,12037,8330],{"class":4036},[137,12039,4053],{"class":157},[137,12041,12042,12044,12046],{"class":139,"line":4721},[137,12043,4083],{"class":157},[137,12045,7821],{"class":4036},[137,12047,4053],{"class":157},[27,12049,12050],{},[63,12051],{"alt":65,"src":12052},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1649848849\u002Fblog\u002Fgsap-animation-with-vue3-and-vite\u002Fgetting-started-with-gsap-in-vue-iyigej",[27,12054,12055,12056,12059,12060,1017],{},"We can also use ",[22,12057,12058],{},"-=1",". In this case, the animation will start 1 second before the end of the timeline. Hence, creates overlaps. The full documentation of the timeline position parameters can be found at this ",[45,12061,2726],{"href":12062,"target":2716,"rel":12063},"https:\u002F\u002Fgreensock.com\u002Fdocs\u002Fv3\u002FGSAP\u002FTimeline",[2718,2719],[104,12065,12067],{"id":12066},"controls-in-timeline","Controls in timeline",[27,12069,12070],{},"If we want a timeline repeated, we can do so by specifying a repeat property in the configuration object in the timeline.",[128,12072,12074],{"className":130,"code":12073,"language":132,"meta":133,"style":133},"let tl = gsap.timeline({ repeat: 2 });\n",[22,12075,12076],{"__ignoreMap":133},[137,12077,12078,12080,12082,12084,12086,12088,12091,12093],{"class":139,"line":140},[137,12079,11498],{"class":143},[137,12081,11501],{"class":157},[137,12083,253],{"class":143},[137,12085,11506],{"class":157},[137,12087,11509],{"class":147},[137,12089,12090],{"class":157},"({ repeat: ",[137,12092,10345],{"class":364},[137,12094,4168],{"class":157},[27,12096,12097],{},"By assigning a repeat value of 2, our timeline will play once regularly for the first time, and then repeat two more times. If we want to repeat infinitely, then we use a value of -1.",[27,12099,12100,12101,12104,12105,10928,12107,12109],{},"Another property that is very useful with repeats, is called ",[22,12102,12103],{},"yoyo",". If we set ",[22,12106,12103],{},[22,12108,3097],{}," then our timeline will play regularly the first time, but go backwards on the second time. The timeline will repeat infinitely by going forward and backwards.",[128,12111,12113],{"className":130,"code":12112,"language":132,"meta":133,"style":133},"let tl = gsap.timeline({ repeat: 2, yoyo: true });\n",[22,12114,12115],{"__ignoreMap":133},[137,12116,12117,12119,12121,12123,12125,12127,12129,12131,12134,12136],{"class":139,"line":140},[137,12118,11498],{"class":143},[137,12120,11501],{"class":157},[137,12122,253],{"class":143},[137,12124,11506],{"class":157},[137,12126,11509],{"class":147},[137,12128,12090],{"class":157},[137,12130,10345],{"class":364},[137,12132,12133],{"class":157},", yoyo: ",[137,12135,3097],{"class":364},[137,12137,4168],{"class":157},[27,12139,12097],{},[27,12141,12100,12142,12104,12144,10928,12146,12109],{},[22,12143,12103],{},[22,12145,12103],{},[22,12147,3097],{},[128,12149,12150],{"className":130,"code":12112,"language":132,"meta":133,"style":133},[22,12151,12152],{"__ignoreMap":133},[137,12153,12154,12156,12158,12160,12162,12164,12166,12168,12170,12172],{"class":139,"line":140},[137,12155,11498],{"class":143},[137,12157,11501],{"class":157},[137,12159,253],{"class":143},[137,12161,11506],{"class":157},[137,12163,11509],{"class":147},[137,12165,12090],{"class":157},[137,12167,10345],{"class":364},[137,12169,12133],{"class":157},[137,12171,3097],{"class":364},[137,12173,4168],{"class":157},[27,12175,12176],{},"We can further control our animation in timelines by adding one of few methods available such as:",[128,12178,12180],{"className":130,"code":12179,"language":132,"meta":133,"style":133},"tl.play();\ntl.pause();\ntl.resume();\ntl.seek(1.5);\ntl.reverse();\ntl.restart();\n",[22,12181,12182,12191,12200,12209,12223,12232],{"__ignoreMap":133},[137,12183,12184,12186,12189],{"class":139,"line":140},[137,12185,11541],{"class":157},[137,12187,12188],{"class":147},"play",[137,12190,924],{"class":157},[137,12192,12193,12195,12198],{"class":139,"line":173},[137,12194,11541],{"class":157},[137,12196,12197],{"class":147},"pause",[137,12199,924],{"class":157},[137,12201,12202,12204,12207],{"class":139,"line":188},[137,12203,11541],{"class":157},[137,12205,12206],{"class":147},"resume",[137,12208,924],{"class":157},[137,12210,12211,12213,12216,12218,12221],{"class":139,"line":269},[137,12212,11541],{"class":157},[137,12214,12215],{"class":147},"seek",[137,12217,356],{"class":157},[137,12219,12220],{"class":364},"1.5",[137,12222,1502],{"class":157},[137,12224,12225,12227,12230],{"class":139,"line":278},[137,12226,11541],{"class":157},[137,12228,12229],{"class":147},"reverse",[137,12231,924],{"class":157},[137,12233,12234,12236,12239],{"class":139,"line":291},[137,12235,11541],{"class":157},[137,12237,12238],{"class":147},"restart",[137,12240,924],{"class":157},[27,12242,12243,12244,12248],{},"If you are curious to see all the above methods in action please view my app on Github ",[45,12245,10647],{"href":12246,"target":2716,"rel":12247},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fgsap-animation-with-vue3-and-vite",[2718,2719],". I have developed a small Vue 3 app with GSAP timeline animation where all the above methods are demonstrated. I hope you will find it useful.",[104,12250,2567],{"id":2566},[2569,12252,12253,12256,12262,12275,12284],{},[1006,12254,12255],{},"GreenSock Animation Platform (GSAP) is the most robust JavaScript animation library to date that allows developers to animate literaly any DOM element with a breeze.",[1006,12257,12258,12259,12261],{},"We can add a CSS class or an ID as a string, and GSAP can magically find that DOM element. Since we are using Vue, it is best practice to use template ",[22,12260,10353],{}," for that purpose.",[1006,12263,12264,12265,164,12267,164,12270,164,12272,12274],{},"The configuration object in GSAP has some special properties such as ",[22,12266,10367],{},[22,12268,12269],{},"delay, ease",[22,12271,10938],{},[22,12273,2147],{}," that can be used to add or enhance an animation.",[1006,12276,10731,12277,164,12279,164,12281,12283],{},[22,12278,10734],{},[22,12280,10737],{},[22,12282,10740],{}," etc. that can be used to define custom functionalities.",[1006,12285,12286],{},"The timeline feature in GSAP allows us to easily build complex animations.",[2617,12288,12289],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":133,"searchDepth":173,"depth":173,"links":12291},[12292,12293,12294,12295,12297,12299,12300,12301],{"id":9522,"depth":173,"text":9523},{"id":10252,"depth":173,"text":10253},{"id":10304,"depth":173,"text":10305},{"id":10935,"depth":173,"text":12296},"stagger special property",{"id":11339,"depth":173,"text":12298},"delay special property",{"id":11484,"depth":173,"text":11485},{"id":12066,"depth":173,"text":12067},{"id":2566,"depth":173,"text":2567},"GreenSock Animation Platform (GSAP) is the most robust JavaScript animation library to date that allows developers to animate literally any DOM element with a breeze. GSAP provides an API that can be used for complex animation to be created. Hence, is still supported by all major browsers. In comparison to CSS animation, sequencing in GSAP is very easy. So let's have a look at GSAP. We are going to use GSAP in a Vue 3 project. We will use Vite as a build tool. In this blog article, we are going to animate DOM elements. GSAP is also capable of animating SVG, Canvas, WebGL, JS object etc. The full documentation of GSAP can be found at this.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1649846664\u002Fblog\u002Fgsap-animation-with-vue3-and-vite\u002Fgsap-animation-with-vue3-and-vite",[10155,8,12305,9,12306,12307,5299,5300,7531],"Animation","GSAP","Stagger",{},"\u002F2022\u002F04\u002F14\u002Fgsap-animation-with-vue3-and-vite","14th Apr 2022",{"title":10155,"description":12302},"2022\u002F04\u002F14\u002Fgsap-animation-with-vue3-and-vite","53T8uz5nPhdV8Qmh1EwtCUxUWt7e3z461UMR8bMRE9s",{"id":12315,"title":12316,"articleTags":12317,"author":11,"blog":12,"body":12318,"description":12801,"extension":2649,"image":12802,"keywords":12803,"meta":12806,"navigation":515,"path":12807,"published":12808,"readTime":269,"seo":12809,"stem":12810,"type":2662,"__hash__":12811},"content\u002F2022\u002F04\u002F18\u002Freload-child-component-in-vue3.md","Reload Child component in Vue 3",[10,8,9],{"type":14,"value":12319,"toc":12792},[12320,12323,12337,12339,12343,12348,12355,12358,12360,12363,12377,12380,12408,12412,12426,12443,12532,12543,12554,12611,12621,12627,12634,12734,12756,12762,12767,12773,12775,12789],[17,12321,12316],{"id":12322},"reload-child-component-in-vue-3",[27,12324,12325],{},[30,12326,12327,36,12329,40,12331],{},[33,12328],{"value":35},[33,12330],{"value":39},[42,12332,12333],{},[45,12334,12335],{"href":47},[33,12336],{"value":50},[52,12338],{":tags":54},[56,12340],{":audio-src":12341,":transcript-src":12342},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F04\u002F18\u002Freload-child-component-in-vue3\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F04\u002F18\u002Freload-child-component-in-vue3\u002Fsummary.json",[27,12344,12345],{},[63,12346],{"alt":65,"src":12347},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1650277793\u002Fblog\u002Freload-child-component-in-vue-3\u002Freload-child-component-in-vue-3-1_ccze7d",[27,12349,12350,12351,12354],{},"Components in Vue are reusable custom elements (for example ",[22,12352,12353],{},"\u003Ccard>"," ) that can be reused in Vue templates throughout the app. In a large scale application, we will come across a situation where we have nested components such as parents and children.",[27,12356,12357],{},"In recent work, I faced the challenge of having to reload the child component only. This article will explain the intricacies to the challenge. Looking back, it took me unbelievably long to figure out a solution. Nevertheless, my blog post will provide quick and easy, short, sharp steps to any developer who might be faced with the same challenge. Let's get started.",[104,12359,9523],{"id":9522},[27,12361,12362],{},"We can easily scaffold our Vue 3 project using Vite with the following command:",[128,12364,12365],{"className":8665,"code":9529,"language":8667,"meta":133,"style":133},[22,12366,12367],{"__ignoreMap":133},[137,12368,12369,12371,12373,12375],{"class":139,"line":140},[137,12370,9536],{"class":147},[137,12372,9539],{"class":284},[137,12374,9542],{"class":284},[137,12376,9545],{"class":284},[27,12378,12379],{},"Next, we follow the prompts, and choose Vue with JavaScript. Lastly, we install the dependency and run the project locally:",[128,12381,12382],{"className":8665,"code":9551,"language":8667,"meta":133,"style":133},[22,12383,12384,12390,12394,12400],{"__ignoreMap":133},[137,12385,12386,12388],{"class":139,"line":140},[137,12387,9558],{"class":364},[137,12389,9545],{"class":284},[137,12391,12392],{"class":139,"line":173},[137,12393,516],{"emptyLinePlaceholder":515},[137,12395,12396,12398],{"class":139,"line":188},[137,12397,9536],{"class":147},[137,12399,9571],{"class":284},[137,12401,12402,12404,12406],{"class":139,"line":269},[137,12403,9536],{"class":147},[137,12405,9578],{"class":284},[137,12407,9581],{"class":284},[104,12409,12411],{"id":12410},"make-a-component-that-displays-a-random-number","Make a component that displays a random number",[27,12413,12414,12415,12419,12420,12422,12423,12425],{},"Use the new ",[45,12416,9597],{"href":12417,"target":2716,"rel":12418},"https:\u002F\u002Fvuejs.org\u002Fguide\u002Fextras\u002Fcomposition-api-faq.html",[2718,2719]," syntax with ",[22,12421,9591],{}," . The ",[22,12424,9591],{}," feature was released in Vue 3.2.",[27,12427,9601,12428,9607,12433,12438,12439,9628],{},[42,12429,12430],{},[30,12431,12432],{},"RandomNumber.vue",[42,12434,12435],{},[30,12436,12437],{},"src\u002Fcomponents\u002FRandomNumber.vue"," directory. Next, create a reactive variable and assign a random number when the component mounts. Let's look at the following code in the ",[42,12440,12441],{},[30,12442,12432],{},[128,12444,12446],{"className":130,"code":12445,"language":132,"meta":133,"style":133},"\u003Cscript setup>\nimport { ref, onMounted } from \"vue\";\n\nconst randomNumber = ref(0);\n\nonMounted(() => {\n    randomNumber.value = Math.floor(Math.random() * 100);\n});\n\u003C\u002Fscript>\n\n\u003Ctemplate>{{ randomNumber }}\u003C\u002Ftemplate>\n",[22,12447,12448,12458,12462,12466,12471,12475,12479,12503,12507,12515,12519],{"__ignoreMap":133},[137,12449,12450,12452,12454,12456],{"class":139,"line":140},[137,12451,4033],{"class":157},[137,12453,4037],{"class":4036},[137,12455,9642],{"class":147},[137,12457,4053],{"class":157},[137,12459,12460],{"class":139,"line":173},[137,12461,10395],{"class":157},[137,12463,12464],{"class":139,"line":188},[137,12465,516],{"emptyLinePlaceholder":515},[137,12467,12468],{"class":139,"line":269},[137,12469,12470],{"class":157},"const randomNumber = ref(0);\n",[137,12472,12473],{"class":139,"line":278},[137,12474,516],{"emptyLinePlaceholder":515},[137,12476,12477],{"class":139,"line":291},[137,12478,10417],{"class":157},[137,12480,12481,12484,12486,12488,12490,12492,12494,12496,12498,12501],{"class":139,"line":297},[137,12482,12483],{"class":157},"    randomNumber.value ",[137,12485,253],{"class":143},[137,12487,7675],{"class":157},[137,12489,8011],{"class":147},[137,12491,8014],{"class":157},[137,12493,7678],{"class":147},[137,12495,3348],{"class":157},[137,12497,7672],{"class":143},[137,12499,12500],{"class":364}," 100",[137,12502,1502],{"class":157},[137,12504,12505],{"class":139,"line":302},[137,12506,5422],{"class":157},[137,12508,12509,12511,12513],{"class":139,"line":662},[137,12510,4083],{"class":157},[137,12512,4037],{"class":4036},[137,12514,4053],{"class":157},[137,12516,12517],{"class":139,"line":667},[137,12518,516],{"emptyLinePlaceholder":515},[137,12520,12521,12523,12525,12528,12530],{"class":139,"line":786},[137,12522,4033],{"class":157},[137,12524,7821],{"class":4036},[137,12526,12527],{"class":157},">{{ randomNumber }}\u003C\u002F",[137,12529,7821],{"class":4036},[137,12531,4053],{"class":157},[104,12533,9726,12535],{"id":12534},"use-randomnumber-component-in-our-appvue",[42,12536,12537],{},[30,12538,12539,12542],{},[22,12540,12541],{},"\u003CRandomNumber>"," component in our App.vue",[27,12544,4286,12545,9737,12549,12553],{},[42,12546,12547],{},[30,12548,9736],{},[42,12550,12551],{},[30,12552,12432],{}," component and then use it in our template.",[128,12555,12557],{"className":130,"code":12556,"language":132,"meta":133,"style":133},"\u003Cscript setup>\nimport RandomNumber from \".\u002Fcomponents\u002FRandomNumber.vue\";\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003CRandomNumber \u002F>\n\u003C\u002Ftemplate>\n",[22,12558,12559,12569,12574,12582,12586,12594,12603],{"__ignoreMap":133},[137,12560,12561,12563,12565,12567],{"class":139,"line":140},[137,12562,4033],{"class":157},[137,12564,4037],{"class":4036},[137,12566,9642],{"class":147},[137,12568,4053],{"class":157},[137,12570,12571],{"class":139,"line":173},[137,12572,12573],{"class":157},"import RandomNumber from \".\u002Fcomponents\u002FRandomNumber.vue\";\n",[137,12575,12576,12578,12580],{"class":139,"line":188},[137,12577,4083],{"class":157},[137,12579,4037],{"class":4036},[137,12581,4053],{"class":157},[137,12583,12584],{"class":139,"line":269},[137,12585,516],{"emptyLinePlaceholder":515},[137,12587,12588,12590,12592],{"class":139,"line":278},[137,12589,4033],{"class":157},[137,12591,7821],{"class":4036},[137,12593,4053],{"class":157},[137,12595,12596,12598,12601],{"class":139,"line":291},[137,12597,4072],{"class":157},[137,12599,12600],{"class":364},"RandomNumber",[137,12602,4078],{"class":157},[137,12604,12605,12607,12609],{"class":139,"line":297},[137,12606,4083],{"class":157},[137,12608,7821],{"class":4036},[137,12610,4053],{"class":157},[104,12612,12614,12615],{"id":12613},"reload-our-randomnumber-component","Reload our ",[42,12616,12617],{},[30,12618,12619,9729],{},[22,12620,12541],{},[27,12622,12623,12624,12626],{},"Now, whenever we import our component we have a random number displayed. When we reload our webpage, a different number is shown. But how can we reload only the ",[22,12625,12541],{}," component?",[27,12628,12629,12630,12633],{},"That can be made possible by binding the ",[22,12631,12632],{},"key"," value to the component. Each time we assign a different key value, the component reloads. Let's see how we can achieve that:",[128,12635,12637],{"className":130,"code":12636,"language":132,"meta":133,"style":133},"\u003Cscript setup>\nimport { ref } from \"vue\";\nimport RandomNumber from \".\u002Fcomponents\u002FRandomNumber.vue\";\n\nconst keyIndex = ref(0);\n\nfunction changeKey() {\n    keyIndex.value++;\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003CRandomNumber :key=\"keyIndex\" \u002F>\n    \u003Cbutton @click=\"changeKey\">Re-Render\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n",[22,12638,12639,12649,12654,12658,12662,12667,12671,12676,12686,12690,12698,12702,12710,12721,12730],{"__ignoreMap":133},[137,12640,12641,12643,12645,12647],{"class":139,"line":140},[137,12642,4033],{"class":157},[137,12644,4037],{"class":4036},[137,12646,9642],{"class":147},[137,12648,4053],{"class":157},[137,12650,12651],{"class":139,"line":173},[137,12652,12653],{"class":157},"import { ref } from \"vue\";\n",[137,12655,12656],{"class":139,"line":188},[137,12657,12573],{"class":157},[137,12659,12660],{"class":139,"line":269},[137,12661,516],{"emptyLinePlaceholder":515},[137,12663,12664],{"class":139,"line":278},[137,12665,12666],{"class":157},"const keyIndex = ref(0);\n",[137,12668,12669],{"class":139,"line":291},[137,12670,516],{"emptyLinePlaceholder":515},[137,12672,12673],{"class":139,"line":297},[137,12674,12675],{"class":157},"function changeKey() {\n",[137,12677,12678,12681,12684],{"class":139,"line":302},[137,12679,12680],{"class":157},"    keyIndex.value",[137,12682,12683],{"class":143},"++",[137,12685,3276],{"class":157},[137,12687,12688],{"class":139,"line":662},[137,12689,510],{"class":157},[137,12691,12692,12694,12696],{"class":139,"line":667},[137,12693,4083],{"class":157},[137,12695,4037],{"class":4036},[137,12697,4053],{"class":157},[137,12699,12700],{"class":139,"line":786},[137,12701,516],{"emptyLinePlaceholder":515},[137,12703,12704,12706,12708],{"class":139,"line":798},[137,12705,4033],{"class":157},[137,12707,7821],{"class":4036},[137,12709,4053],{"class":157},[137,12711,12712,12714,12716,12719],{"class":139,"line":803},[137,12713,4072],{"class":157},[137,12715,12600],{"class":364},[137,12717,12718],{"class":8180}," :key=\"keyIndex\"",[137,12720,4078],{"class":157},[137,12722,12723,12725,12727],{"class":139,"line":931},[137,12724,4072],{"class":157},[137,12726,8170],{"class":4036},[137,12728,12729],{"class":8180}," @click=\"changeKey\">Re-Render\u003C\u002Fbutton>\n",[137,12731,12732],{"class":139,"line":1568},[137,12733,8189],{"class":8180},[27,12735,12736,12737,12740,12741,12743,12744,12747,12748,12750,12751,12753,12754,1902],{},"In the code above, we've create a button whereby upon each click of the button, the function ",[22,12738,12739],{},"changeKey"," is activated. The ",[22,12742,12739],{}," increases the number of the reactive variable, ",[22,12745,12746],{},"keyIndex",", upon each click. We bind the ",[22,12749,12632],{}," of the ",[22,12752,12600],{}," with the ",[22,12755,12746],{},[27,12757,12758,12759,12761],{},"Now, with each click of the button, only the ",[22,12760,12541],{}," component reloads. Hence, a different number is displayed.",[27,12763,12764],{},[63,12765],{"alt":65,"src":12766},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1650277793\u002Fblog\u002Freload-child-component-in-vue-3\u002Freload-child-component-in-vue-3-2_gnt1zf",[27,12768,10126,12769,1017],{},[45,12770,2726],{"href":12771,"target":2716,"rel":12772},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Freload-child-component-in-vue3",[2718,2719],[104,12774,2567],{"id":2566},[2569,12776,12777,12780,12786],{},[1006,12778,12779],{},"In a large scale application we will come across a situation where we have nested components such as parents and children.",[1006,12781,12782,12783,12785],{},"To be able to reload only the child component, and not the entire website, we need to assign a ",[22,12784,12632],{}," to the component.",[1006,12787,12788],{},"Each time we assign different key to the component, the component automatically reloads.",[2617,12790,12791],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":133,"searchDepth":173,"depth":173,"links":12793},[12794,12795,12796,12798,12800],{"id":9522,"depth":173,"text":9523},{"id":12410,"depth":173,"text":12411},{"id":12534,"depth":173,"text":12797},"Use \u003CRandomNumber> component in our App.vue",{"id":12613,"depth":173,"text":12799},"Reload our \u003CRandomNumber> component",{"id":2566,"depth":173,"text":2567},"Components in Vue are reusable custom elements that can be reused in Vue templates throughout the app. In a large scale application, we will come across a situation where we have nested components such as parents and children. In recent work, I faced the challenge of having to reload the child component only. This article will explain the intricacies to the challenge. Looking back, it took me unbelievably long to figure out a solution. Nevertheless, my blog post will provide quick and easy, short, sharp steps to any developer who might be faced with the same challenge. Let's get started. We can easily scaffold our Vue 3 project using Vite with the following command","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1650277793\u002Fblog\u002Freload-child-component-in-vue-3\u002Freload-child-component-in-vue-3-1_ccze7d",[12316,8,12804,9,12805,9597,5299,5300],"Reload","Tips",{},"\u002F2022\u002F04\u002F18\u002Freload-child-component-in-vue3","18th Apr 2022",{"title":12316,"description":12801},"2022\u002F04\u002F18\u002Freload-child-component-in-vue3","Bd6tMRAZo6e-jB83d-1jMjBiPQ6wP10NXq646cUqoNI",{"id":12813,"title":12814,"articleTags":12815,"author":11,"blog":12,"body":12818,"description":16627,"extension":2649,"image":16628,"keywords":16629,"meta":16632,"navigation":515,"path":16633,"published":16634,"readTime":1573,"seo":16635,"stem":16636,"type":2662,"__hash__":16637},"content\u002F2022\u002F10\u002F03\u002Fnestjs-auth-authorisation-with-okta.md","Nest.js Auth\u002FAuthorisation with Okta",[12816,12817,2669],"Nest.js","TypeScript",{"type":14,"value":12819,"toc":16608},[12820,12823,12837,12839,12843,12849,12853,12861,12876,12879,12883,12906,12909,12913,12922,12939,12942,12957,12963,12997,13029,13036,13063,13076,13090,13095,13100,13105,13110,13115,13118,13122,13125,13132,13135,13141,13144,13150,13153,13156,13160,13163,13166,13184,13198,13201,13221,13231,13234,13252,13256,13259,13266,13284,13297,13465,13497,13503,13509,13513,13516,13522,13525,13530,13555,13562,13569,13576,13690,13696,13779,13782,13786,13789,13796,13810,13819,13835,13840,14346,14359,14447,14450,14453,14457,14466,14480,14483,14487,14494,14500,14503,14509,14512,14518,14522,14532,14552,14572,14973,14985,14997,15125,15129,15136,15148,15158,15313,15318,15437,15452,15760,15766,15917,15920,15924,15943,15946,16001,16009,16017,16064,16068,16081,16222,16225,16253,16258,16457,16460,16467,16471,16474,16502,16510,16525,16528,16556,16559,16565,16567,16605],[17,12821,12814],{"id":12822},"nestjs-authauthorisation-with-okta",[27,12824,12825],{},[30,12826,12827,36,12829,40,12831],{},[33,12828],{"value":35},[33,12830],{"value":39},[42,12832,12833],{},[45,12834,12835],{"href":47},[33,12836],{"value":50},[52,12838],{":tags":54},[56,12840],{":audio-src":12841,":transcript-src":12842},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F10\u002F03\u002Fnestjs-auth-authorisation-with-okta\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F10\u002F03\u002Fnestjs-auth-authorisation-with-okta\u002Fsummary.json",[27,12844,12845],{},[63,12846],{"alt":12847,"src":12848},"Landing Image","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1664708491\u002Fblog\u002Fnestjs-auth-authorisation-with-okta\u002Fnestjs-auth-authorisation-with-okta",[104,12850,12852],{"id":12851},"what-is-nest-nestjs","What is Nest (NestJS)?",[27,12854,12855,12860],{},[45,12856,12859],{"href":12857,"target":2716,"rel":12858},"https:\u002F\u002Fnestjs.com\u002F",[2718,2719],"Nest"," is a progressive Node.js framework for building efficient, reliable and scalable server-side applications with TypeScript.",[27,12862,12863,12864,12869,12870,12875],{},"Under the hood, Nest makes use of two javascript backend frameworks ",[45,12865,12868],{"href":12866,"target":2716,"rel":12867},"https:\u002F\u002Fexpressjs.com\u002F",[2718,2719],"Express"," (the default) and can also be configured to use with ",[45,12871,12874],{"href":12872,"target":2716,"rel":12873},"https:\u002F\u002Fwww.fastify.io\u002F",[2718,2719],"Fastify"," as well!",[27,12877,12878],{},"The examples in this blog article leverages on the default Express framework.",[104,12880,12882],{"id":12881},"what-is-okta","What is Okta?",[27,12884,12885,12890,12891,164,12896,164,12901,10639],{},[45,12886,12889],{"href":12887,"target":2716,"rel":12888},"https:\u002F\u002Fwww.okta.com\u002F",[2718,2719],"Okta"," is an Identity as a Service (IDaaS). This is a cloud-based authentication or identity management subscription service. Okta can be used for a number of different applications such as ",[45,12892,12895],{"href":12893,"target":2716,"rel":12894},"https:\u002F\u002Fwww.okta.com\u002Fau\u002Fblog\u002F2016\u002F12\u002Ftwo-factor-authentication-vs-multi-factor-authentication-what-are-the-risks\u002F",[2718,2719],"Adaptive multi-factor authentication",[45,12897,12900],{"href":12898,"target":2716,"rel":12899},"https:\u002F\u002Fwww.okta.com\u002Fau\u002Fproducts\u002Fsingle-sign-on\u002F",[2718,2719],"single sign-on",[45,12902,12905],{"href":12903,"target":2716,"rel":12904},"https:\u002F\u002Fwww.okta.com\u002Fau\u002Fproducts\u002Funiversal-directory\u002F",[2718,2719],"Universal Directory",[27,12907,12908],{},"In this blog article we will be creating a Nest application where users (with different roles) can sign-up and sign-in to the application. Specific permissions can be configured for each user access to specific endpoints, based on the user role. We are going to use Okta to help us with user Authentication and Authorisation.",[104,12910,12912],{"id":12911},"create-a-nest-project","Create a Nest Project",[27,12914,12915,12916,12921],{},"Before we continue let's first install ",[45,12917,12920],{"href":12918,"target":2716,"rel":12919},"https:\u002F\u002Fdocs.nestjs.com\u002Fcli\u002Foverview",[2718,2719],"Nest CLI",". That will enable us to efficiently create this project. To install Nest CLI globally, use the following command in your terminal:",[128,12923,12925],{"className":8665,"code":12924,"language":8667,"meta":133,"style":133},"npm install -g @nestjs\u002Fcli\n",[22,12926,12927],{"__ignoreMap":133},[137,12928,12929,12931,12933,12936],{"class":139,"line":140},[137,12930,9536],{"class":147},[137,12932,10268],{"class":284},[137,12934,12935],{"class":364}," -g",[137,12937,12938],{"class":284}," @nestjs\u002Fcli\n",[27,12940,12941],{},"Setting up a new project is quite simple with Nest CLI. We can create a new Nest project with the following commands in our terminal:",[128,12943,12945],{"className":8665,"code":12944,"language":8667,"meta":133,"style":133},"nest new nest-auth-with-okta\n",[22,12946,12947],{"__ignoreMap":133},[137,12948,12949,12952,12954],{"class":139,"line":140},[137,12950,12951],{"class":147},"nest",[137,12953,1426],{"class":284},[137,12955,12956],{"class":284}," nest-auth-with-okta\n",[27,12958,12959,12960,12962],{},"Next, choose ",[22,12961,9536],{}," for this project.",[128,12964,12966],{"className":8665,"code":12965,"language":8667,"meta":133,"style":133},"? Which package manager would you ❤️ to use?\n❯ npm\n  yarn\n  pnpm\n",[22,12967,12968,12979,12987,12992],{"__ignoreMap":133},[137,12969,12970,12973,12976],{"class":139,"line":140},[137,12971,12972],{"class":143},"?",[137,12974,12975],{"class":157}," Which package manager would you ❤️ to use",[137,12977,12978],{"class":143},"?\n",[137,12980,12981,12984],{"class":139,"line":173},[137,12982,12983],{"class":147},"❯",[137,12985,12986],{"class":284}," npm\n",[137,12988,12989],{"class":139,"line":188},[137,12990,12991],{"class":147},"  yarn\n",[137,12993,12994],{"class":139,"line":269},[137,12995,12996],{"class":147},"  pnpm\n",[128,12998,13000],{"className":8665,"code":12999,"language":8667,"meta":133,"style":133},"? Which package manager would you ❤️ to use? npm\n▹▹▹▹▸ Installation in progress... ☕\n",[22,13001,13002,13012],{"__ignoreMap":133},[137,13003,13004,13006,13008,13010],{"class":139,"line":140},[137,13005,12972],{"class":143},[137,13007,12975],{"class":157},[137,13009,12972],{"class":143},[137,13011,12986],{"class":157},[137,13013,13014,13017,13020,13023,13026],{"class":139,"line":173},[137,13015,13016],{"class":147},"▹▹▹▹▸",[137,13018,13019],{"class":284}," Installation",[137,13021,13022],{"class":284}," in",[137,13024,13025],{"class":284}," progress...",[137,13027,13028],{"class":284}," ☕\n",[27,13030,13031,13032,13035],{},"Lastly, navigate to the newly created ",[22,13033,13034],{},"nest-auth-with-okta"," directory and run this project.",[128,13037,13039],{"className":8665,"code":13038,"language":8667,"meta":133,"style":133},"$ cd nest-auth-with-okta\n$ npm run start\n",[22,13040,13041,13051],{"__ignoreMap":133},[137,13042,13043,13046,13049],{"class":139,"line":140},[137,13044,13045],{"class":147},"$",[137,13047,13048],{"class":284}," cd",[137,13050,12956],{"class":284},[137,13052,13053,13055,13058,13060],{"class":139,"line":173},[137,13054,13045],{"class":147},[137,13056,13057],{"class":284}," npm",[137,13059,9578],{"class":284},[137,13061,13062],{"class":284}," start\n",[27,13064,13065,13066,13072,13073],{},"If we navigate to the ",[45,13067,13070],{"href":13068,"rel":13069},"http:\u002F\u002Flocalhost:3000",[10924],[42,13071,13068],{}," in the browser, we will be able to see ",[42,13074,13075],{},"Hello World!",[27,13077,13078,13079,13081,13082,13085,13086,13089],{},"Now, let's have a look at the folder structure in our Nest project. Inside the ",[22,13080,13034],{}," directory we have ",[22,13083,13084],{},"node_modules",", a few other boilerplate files and a ",[22,13087,13088],{},"src\u002F"," directory populated with several core files.",[27,13091,13092],{},[22,13093,13094],{},"app.controller.spec.ts",[27,13096,13097],{},[22,13098,13099],{},"app.controller.ts",[27,13101,13102],{},[22,13103,13104],{},"app.module.ts",[27,13106,13107],{},[22,13108,13109],{},"app.service.ts",[27,13111,13112],{},[22,13113,13114],{},"main.ts",[27,13116,13117],{},"Cool, this means that we have successfully created our Nest project.",[104,13119,13121],{"id":13120},"getting-started-with-okta","Getting started with Okta",[27,13123,13124],{},"We will now create our developer account with Okta.",[27,13126,13127,13128,1017],{},"Okta's Developer Edition provides a free, no time limit access to the most key developer features. You can sign-up ",[45,13129,10647],{"href":13130,"target":2716,"rel":13131},"https:\u002F\u002Fdeveloper.okta.com\u002Fsignup\u002F",[2718,2719],[27,13133,13134],{},"Next, we will create an app integration. We can do so by signing in to the Okta dashboard and navigating to Applications → Applications → Create App Integration.",[27,13136,13137],{},[63,13138],{"alt":13139,"src":13140},"Creating developer account with Okta","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1664763235\u002Fblog\u002Fnestjs-auth-authorisation-with-okta\u002Fnestjs-auth-authorisation-with-okta_ryik8s",[27,13142,13143],{},"From the prompt, we choose API Services and click Next.",[27,13145,13146],{},[63,13147],{"alt":13148,"src":13149},"Creating developer account with Okta, choosing API Services and click Next.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1664763462\u002Fblog\u002Fnestjs-auth-authorisation-with-okta\u002Fnestjs-auth-authorisation-with-okta_i0rynr",[27,13151,13152],{},"Lastly, we enter the name of the our App integration and click Save.",[27,13154,13155],{},"That's all we need for now. Later in the blog article, we will come back to setup some more things. Until then, let's move to the next section where we start implementing our Authentication in Nest.",[104,13157,13159],{"id":13158},"create-a-user-module","Create a user module",[27,13161,13162],{},"We are going to create a user module, user controller and user service. We can do that efficiently with Nest CLI.",[27,13164,13165],{},"First, we create the user module:",[128,13167,13169],{"className":8665,"code":13168,"language":8667,"meta":133,"style":133},"nest g module user\n",[22,13170,13171],{"__ignoreMap":133},[137,13172,13173,13175,13178,13181],{"class":139,"line":140},[137,13174,12951],{"class":147},[137,13176,13177],{"class":284}," g",[137,13179,13180],{"class":284}," module",[137,13182,13183],{"class":284}," user\n",[27,13185,13186,13187,13190,13191,13194,13195,13197],{},"Nest CLI will automatically create ",[22,13188,13189],{},"user.module.ts"," file under the ",[22,13192,13193],{},"user"," directory and update the ",[22,13196,13104],{}," file accordingly.",[27,13199,13200],{},"Next, we create the controller:",[128,13202,13204],{"className":8665,"code":13203,"language":8667,"meta":133,"style":133},"nest g controller user --no-spec\n",[22,13205,13206],{"__ignoreMap":133},[137,13207,13208,13210,13212,13215,13218],{"class":139,"line":140},[137,13209,12951],{"class":147},[137,13211,13177],{"class":284},[137,13213,13214],{"class":284}," controller",[137,13216,13217],{"class":284}," user",[137,13219,13220],{"class":364}," --no-spec\n",[27,13222,13223,13224,13227,13228,10277],{},"You would have noticed that we use ",[22,13225,13226],{},"--no-spec"," flag at the end of the command. This allows us to exclude the ",[22,13229,13230],{},"user.controller.spec.ts",[27,13232,13233],{},"Lastly, we create the service file:",[128,13235,13237],{"className":8665,"code":13236,"language":8667,"meta":133,"style":133},"nest g service user --no-spec\n",[22,13238,13239],{"__ignoreMap":133},[137,13240,13241,13243,13245,13248,13250],{"class":139,"line":140},[137,13242,12951],{"class":147},[137,13244,13177],{"class":284},[137,13246,13247],{"class":284}," service",[137,13249,13217],{"class":284},[137,13251,13220],{"class":364},[104,13253,13255],{"id":13254},"nest-configuration","Nest Configuration",[27,13257,13258],{},"Applications often run in different environments. Depending on the environment, different configuration settings should be used.",[27,13260,13261,13262,13265],{},"Nest provides a ",[22,13263,13264],{},"@nestjs\u002Fconfig"," package out-of-the box. In order to setup our configuration, we need to install it first.",[128,13267,13269],{"className":8665,"code":13268,"language":8667,"meta":133,"style":133},"npm i --save @nestjs\u002Fconfig\n",[22,13270,13271],{"__ignoreMap":133},[137,13272,13273,13275,13278,13281],{"class":139,"line":140},[137,13274,9536],{"class":147},[137,13276,13277],{"class":284}," i",[137,13279,13280],{"class":364}," --save",[137,13282,13283],{"class":284}," @nestjs\u002Fconfig\n",[27,13285,13286,13287,13290,13291,13293,13294,13296],{},"Once the installation process is complete, we import the ",[22,13288,13289],{},"ConfigModule",". We need to import the module into the root ",[22,13292,13104],{}," file. Next, add the config module into the imports array. The code inside ",[22,13295,13104],{}," now will look like this:",[128,13298,13302],{"className":13299,"code":13300,"language":13301,"meta":133,"style":133},"language-ts shiki shiki-themes github-light github-dark","import { Module } from \"@nestjs\u002Fcommon\";\nimport { AppController } from \".\u002Fapp.controller\";\nimport { AppService } from \".\u002Fapp.service\";\nimport { UserModule } from \".\u002Fuser\u002Fuser.module\";\nimport { ConfigModule } from \"@nestjs\u002Fconfig\";\n\n@Module({\n    imports: [\n        ConfigModule.forRoot({\n            isGlobal: true,\n            envFilePath: \".env\",\n        }),\n        UserModule,\n    ],\n    controllers: [AppController],\n    providers: [AppService],\n})\nexport class AppModule {}\n","ts",[22,13303,13304,13318,13332,13346,13360,13374,13378,13388,13393,13403,13412,13422,13427,13432,13437,13442,13447,13452],{"__ignoreMap":133},[137,13305,13306,13308,13311,13313,13316],{"class":139,"line":140},[137,13307,10287],{"class":143},[137,13309,13310],{"class":157}," { Module } ",[137,13312,10954],{"class":143},[137,13314,13315],{"class":284}," \"@nestjs\u002Fcommon\"",[137,13317,3276],{"class":157},[137,13319,13320,13322,13325,13327,13330],{"class":139,"line":173},[137,13321,10287],{"class":143},[137,13323,13324],{"class":157}," { AppController } ",[137,13326,10954],{"class":143},[137,13328,13329],{"class":284}," \".\u002Fapp.controller\"",[137,13331,3276],{"class":157},[137,13333,13334,13336,13339,13341,13344],{"class":139,"line":188},[137,13335,10287],{"class":143},[137,13337,13338],{"class":157}," { AppService } ",[137,13340,10954],{"class":143},[137,13342,13343],{"class":284}," \".\u002Fapp.service\"",[137,13345,3276],{"class":157},[137,13347,13348,13350,13353,13355,13358],{"class":139,"line":269},[137,13349,10287],{"class":143},[137,13351,13352],{"class":157}," { UserModule } ",[137,13354,10954],{"class":143},[137,13356,13357],{"class":284}," \".\u002Fuser\u002Fuser.module\"",[137,13359,3276],{"class":157},[137,13361,13362,13364,13367,13369,13372],{"class":139,"line":278},[137,13363,10287],{"class":143},[137,13365,13366],{"class":157}," { ConfigModule } ",[137,13368,10954],{"class":143},[137,13370,13371],{"class":284}," \"@nestjs\u002Fconfig\"",[137,13373,3276],{"class":157},[137,13375,13376],{"class":139,"line":291},[137,13377,516],{"emptyLinePlaceholder":515},[137,13379,13380,13383,13386],{"class":139,"line":297},[137,13381,13382],{"class":157},"@",[137,13384,13385],{"class":147},"Module",[137,13387,3175],{"class":157},[137,13389,13390],{"class":139,"line":302},[137,13391,13392],{"class":157},"    imports: [\n",[137,13394,13395,13398,13401],{"class":139,"line":662},[137,13396,13397],{"class":157},"        ConfigModule.",[137,13399,13400],{"class":147},"forRoot",[137,13402,3175],{"class":157},[137,13404,13405,13408,13410],{"class":139,"line":667},[137,13406,13407],{"class":157},"            isGlobal: ",[137,13409,3097],{"class":364},[137,13411,1961],{"class":157},[137,13413,13414,13417,13420],{"class":139,"line":786},[137,13415,13416],{"class":157},"            envFilePath: ",[137,13418,13419],{"class":284},"\".env\"",[137,13421,1961],{"class":157},[137,13423,13424],{"class":139,"line":798},[137,13425,13426],{"class":157},"        }),\n",[137,13428,13429],{"class":139,"line":803},[137,13430,13431],{"class":157},"        UserModule,\n",[137,13433,13434],{"class":139,"line":931},[137,13435,13436],{"class":157},"    ],\n",[137,13438,13439],{"class":139,"line":1568},[137,13440,13441],{"class":157},"    controllers: [AppController],\n",[137,13443,13444],{"class":139,"line":1573},[137,13445,13446],{"class":157},"    providers: [AppService],\n",[137,13448,13449],{"class":139,"line":1578},[137,13450,13451],{"class":157},"})\n",[137,13453,13454,13457,13459,13462],{"class":139,"line":1588},[137,13455,13456],{"class":143},"export",[137,13458,7832],{"class":143},[137,13460,13461],{"class":147}," AppModule",[137,13463,13464],{"class":157}," {}\n",[27,13466,4370,13467,13470,13471,13473,13474,13477,13478,13480,13481,13483,13484,13477,13487,13490,13491,13493,13494,1017],{},[22,13468,13469],{},"ConfigModule.forRoot"," we pass an object to further configure the ",[22,13472,13289],{},". First, set the property ",[22,13475,13476],{},"isGlobal"," to be ",[22,13479,3097],{},". This will help us to use ",[22,13482,13289],{}," in other modules so we don't need to import our module into each module we use. Secondly, set ",[22,13485,13486],{},"envFilePath",[22,13488,13489],{},".env",". This will load and parse a ",[22,13492,13489],{}," so that we can access the environmental variable throughout the app using ",[22,13495,13496],{},"process.env",[27,13498,13499,13500,13502],{},"Lastly, create a ",[22,13501,13489],{}," file at the root of this project.",[27,13504,13505,13506,13508],{},"We are now all set with the ",[22,13507,13289],{}," . Let's move on to setting up Okta in our Nest app.",[104,13510,13512],{"id":13511},"okta-setup","Okta Setup",[27,13514,13515],{},"Head back to the Okta dashboard to get the Client ID and Okta domain. To find these values, we need to navigate to Applications → Applications → App name set earlier.",[27,13517,13518],{},[63,13519],{"alt":13520,"src":13521},"Okta setup.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1664763933\u002Fblog\u002Fnestjs-auth-authorisation-with-okta\u002Fnestjs-auth-authorisation-with-okta_v853m2",[27,13523,13524],{},"Create a token on the Okta dashboard by navigating to Security → API → Create Token.",[27,13526,13527,13528,9772],{},"Now, with all these values: Client ID, Okta Domain and the Token, we can set the environmental variables in our ",[22,13529,13489],{},[128,13531,13533],{"className":5633,"code":13532,"language":5635,"meta":133,"style":133},"OKTA_ORG_URL=YOUR_OKTA_DOMAIN\nOKTA_TOKEN=YOUR_TOKEN\nOKTA_CLIENT_ID=YOUR_CLIENT_ID\nOKTA_AUDIENCE=api:\u002F\u002Fdefault\n",[22,13534,13535,13540,13545,13550],{"__ignoreMap":133},[137,13536,13537],{"class":139,"line":140},[137,13538,13539],{},"OKTA_ORG_URL=YOUR_OKTA_DOMAIN\n",[137,13541,13542],{"class":139,"line":173},[137,13543,13544],{},"OKTA_TOKEN=YOUR_TOKEN\n",[137,13546,13547],{"class":139,"line":188},[137,13548,13549],{},"OKTA_CLIENT_ID=YOUR_CLIENT_ID\n",[137,13551,13552],{"class":139,"line":269},[137,13553,13554],{},"OKTA_AUDIENCE=api:\u002F\u002Fdefault\n",[27,13556,13557,13558,13561],{},"For the Audience, we set ",[22,13559,13560],{},"api:\u002F\u002Fdefault"," that is the default value in Okta.",[27,13563,13564,13565,13568],{},"Next, we create a ",[22,13566,13567],{},"config\u002F"," directory at the root of the Nest project. Inside this directory, create two files:",[27,13570,13571,13572,13575],{},"File 1: ",[22,13573,13574],{},"auth.config.ts"," and place the following code:",[128,13577,13579],{"className":13299,"code":13578,"language":13301,"meta":133,"style":133},"import { Injectable } from \"@nestjs\u002Fcommon\";\n\n@Injectable()\nexport class AuthConfig {\n    public issuer: string = process.env.OKTA_ORG_URL + \"oauth2\u002Fdefault\";\n    public clientId: string = process.env.OKTA_CLIENT_ID;\n    public audience: string = process.env.OKTA_AUDIENCE;\n}\n",[22,13580,13581,13594,13598,13607,13618,13646,13666,13686],{"__ignoreMap":133},[137,13582,13583,13585,13588,13590,13592],{"class":139,"line":140},[137,13584,10287],{"class":143},[137,13586,13587],{"class":157}," { Injectable } ",[137,13589,10954],{"class":143},[137,13591,13315],{"class":284},[137,13593,3276],{"class":157},[137,13595,13596],{"class":139,"line":173},[137,13597,516],{"emptyLinePlaceholder":515},[137,13599,13600,13602,13605],{"class":139,"line":188},[137,13601,13382],{"class":157},[137,13603,13604],{"class":147},"Injectable",[137,13606,2754],{"class":157},[137,13608,13609,13611,13613,13616],{"class":139,"line":269},[137,13610,13456],{"class":143},[137,13612,7832],{"class":143},[137,13614,13615],{"class":147}," AuthConfig",[137,13617,256],{"class":157},[137,13619,13620,13623,13626,13628,13631,13633,13636,13639,13641,13644],{"class":139,"line":278},[137,13621,13622],{"class":143},"    public",[137,13624,13625],{"class":161}," issuer",[137,13627,894],{"class":143},[137,13629,13630],{"class":364}," string",[137,13632,151],{"class":143},[137,13634,13635],{"class":157}," process.env.",[137,13637,13638],{"class":364},"OKTA_ORG_URL",[137,13640,361],{"class":143},[137,13642,13643],{"class":284}," \"oauth2\u002Fdefault\"",[137,13645,3276],{"class":157},[137,13647,13648,13650,13653,13655,13657,13659,13661,13664],{"class":139,"line":291},[137,13649,13622],{"class":143},[137,13651,13652],{"class":161}," clientId",[137,13654,894],{"class":143},[137,13656,13630],{"class":364},[137,13658,151],{"class":143},[137,13660,13635],{"class":157},[137,13662,13663],{"class":364},"OKTA_CLIENT_ID",[137,13665,3276],{"class":157},[137,13667,13668,13670,13673,13675,13677,13679,13681,13684],{"class":139,"line":297},[137,13669,13622],{"class":143},[137,13671,13672],{"class":161}," audience",[137,13674,894],{"class":143},[137,13676,13630],{"class":364},[137,13678,151],{"class":143},[137,13680,13635],{"class":157},[137,13682,13683],{"class":364},"OKTA_AUDIENCE",[137,13685,3276],{"class":157},[137,13687,13688],{"class":139,"line":302},[137,13689,510],{"class":157},[27,13691,13692,13693,13575],{},"File 2: ",[22,13694,13695],{},"okta.sdk.config.ts",[128,13697,13699],{"className":13299,"code":13698,"language":13301,"meta":133,"style":133},"import { Injectable } from \"@nestjs\u002Fcommon\";\n\n@Injectable()\nexport class OktaSdkConfig {\n    public orgUrl: string = process.env.OKTA_ORG_URL;\n    public token: string = process.env.OKTA_TOKEN;\n}\n",[22,13700,13701,13713,13717,13725,13736,13755,13775],{"__ignoreMap":133},[137,13702,13703,13705,13707,13709,13711],{"class":139,"line":140},[137,13704,10287],{"class":143},[137,13706,13587],{"class":157},[137,13708,10954],{"class":143},[137,13710,13315],{"class":284},[137,13712,3276],{"class":157},[137,13714,13715],{"class":139,"line":173},[137,13716,516],{"emptyLinePlaceholder":515},[137,13718,13719,13721,13723],{"class":139,"line":188},[137,13720,13382],{"class":157},[137,13722,13604],{"class":147},[137,13724,2754],{"class":157},[137,13726,13727,13729,13731,13734],{"class":139,"line":269},[137,13728,13456],{"class":143},[137,13730,7832],{"class":143},[137,13732,13733],{"class":147}," OktaSdkConfig",[137,13735,256],{"class":157},[137,13737,13738,13740,13743,13745,13747,13749,13751,13753],{"class":139,"line":278},[137,13739,13622],{"class":143},[137,13741,13742],{"class":161}," orgUrl",[137,13744,894],{"class":143},[137,13746,13630],{"class":364},[137,13748,151],{"class":143},[137,13750,13635],{"class":157},[137,13752,13638],{"class":364},[137,13754,3276],{"class":157},[137,13756,13757,13759,13762,13764,13766,13768,13770,13773],{"class":139,"line":291},[137,13758,13622],{"class":143},[137,13760,13761],{"class":161}," token",[137,13763,894],{"class":143},[137,13765,13630],{"class":364},[137,13767,151],{"class":143},[137,13769,13635],{"class":157},[137,13771,13772],{"class":364},"OKTA_TOKEN",[137,13774,3276],{"class":157},[137,13776,13777],{"class":139,"line":297},[137,13778,510],{"class":157},[27,13780,13781],{},"In order to use these files across our Nest application, we will first need to import the files and then define them as providers in each of the modules. We will do that later as we progress with our application.",[104,13783,13785],{"id":13784},"define-auth-guards","Define Auth Guards",[27,13787,13788],{},"The purpose of implementing an authentication and authorisation is to be able to protect certain parts of the application to only authenticated users with a specific permissions. This might sound complicated, but with Nest and Okta this implementation is made straightforward.",[27,13790,13791,13792,1017],{},"Nest has Guards to handle all of this out of the box. Guards have a single responsibility. They determine whether a given request should be handled by the route handler or not, depending on certain conditions like permissions or roles. For more details on how Guards work inside Nest, please refer to this ",[45,13793,2726],{"href":13794,"target":2716,"rel":13795},"https:\u002F\u002Fdocs.nestjs.com\u002Fguards",[2718,2719],[27,13797,13798,13799,13801,13802,13805,13806,13809],{},"Let's define our Guard. Create a new directory inside the ",[22,13800,13088],{}," called ",[22,13803,13804],{},"guards\u002F",". Within the directory, create a file ",[22,13807,13808],{},"auth.guard.ts"," where we will place the logic for our Okta authentication Guard.",[27,13811,13812,13813,13818],{},"Before proceeding with the Guard implementation, install and setup the Okta JWT Verifier for Node.js. Install the ",[45,13814,13817],{"href":13815,"target":2716,"rel":13816},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@okta\u002Fjwt-verifier",[2718,2719],"Okta JWT Verifier"," with the following command:",[128,13820,13822],{"className":8665,"code":13821,"language":8667,"meta":133,"style":133},"npm install --save @okta\u002Fjwt-verifier\n",[22,13823,13824],{"__ignoreMap":133},[137,13825,13826,13828,13830,13832],{"class":139,"line":140},[137,13827,9536],{"class":147},[137,13829,10268],{"class":284},[137,13831,13280],{"class":364},[137,13833,13834],{"class":284}," @okta\u002Fjwt-verifier\n",[27,13836,13837,13838,10277],{},"Once the Okta JWT Verifier is installed, implement the Guard logic. You can do so by placing the following code into ",[22,13839,13808],{},[128,13841,13843],{"className":13299,"code":13842,"language":13301,"meta":133,"style":133},"import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from \"@nestjs\u002Fcommon\";\nimport { Observable } from \"rxjs\";\nimport * as OktaJwtVerifier from \"@okta\u002Fjwt-verifier\";\nimport { AuthConfig } from \"..\u002F..\u002Fconfig\u002Fauth.config\";\nimport { Reflector } from \"@nestjs\u002Fcore\";\n\n@Injectable()\nexport class OktaGuard implements CanActivate {\n    oktaJwtVerifier: OktaJwtVerifier;\n\n    constructor(\n        private reflector: Reflector,\n        private readonly authConfig: AuthConfig\n    ) {}\n\n    jwtVerifier(permissions) {\n        this.oktaJwtVerifier = new OktaJwtVerifier({\n            issuer: this.authConfig.issuer,\n            clientId: this.authConfig.clientId,\n            assertClaims: {\n                \"permissions.includes\": permissions,\n            },\n        });\n    }\n\n    canActivate(context: ExecutionContext): boolean | Promise\u003Cboolean> | Observable\u003Cboolean> {\n        const permissions = this.reflector.get\u003Cstring[]>(\"permissions\", context.getHandler());\n\n        this.jwtVerifier(permissions);\n\n        const token = context.getArgs()[0]?.headers?.authorization.split(\" \")[1];\n        return this.oktaJwtVerifier\n            .verifyAccessToken(token, this.authConfig.audience)\n            .then((details) => {\n                console.log(details);\n                return true;\n            })\n            .catch((err) => {\n                console.log(err);\n                throw new UnauthorizedException();\n            });\n    }\n}\n",[22,13844,13845,13858,13872,13892,13906,13920,13924,13932,13949,13961,13965,13971,13986,14001,14006,14010,14022,14037,14047,14057,14062,14070,14075,14080,14084,14088,14137,14174,14178,14190,14194,14229,14238,14254,14271,14280,14289,14294,14311,14320,14332,14337,14341],{"__ignoreMap":133},[137,13846,13847,13849,13852,13854,13856],{"class":139,"line":140},[137,13848,10287],{"class":143},[137,13850,13851],{"class":157}," { Injectable, CanActivate, ExecutionContext, UnauthorizedException } ",[137,13853,10954],{"class":143},[137,13855,13315],{"class":284},[137,13857,3276],{"class":157},[137,13859,13860,13862,13865,13867,13870],{"class":139,"line":173},[137,13861,10287],{"class":143},[137,13863,13864],{"class":157}," { Observable } ",[137,13866,10954],{"class":143},[137,13868,13869],{"class":284}," \"rxjs\"",[137,13871,3276],{"class":157},[137,13873,13874,13876,13879,13882,13885,13887,13890],{"class":139,"line":188},[137,13875,10287],{"class":143},[137,13877,13878],{"class":364}," *",[137,13880,13881],{"class":143}," as",[137,13883,13884],{"class":157}," OktaJwtVerifier ",[137,13886,10954],{"class":143},[137,13888,13889],{"class":284}," \"@okta\u002Fjwt-verifier\"",[137,13891,3276],{"class":157},[137,13893,13894,13896,13899,13901,13904],{"class":139,"line":269},[137,13895,10287],{"class":143},[137,13897,13898],{"class":157}," { AuthConfig } ",[137,13900,10954],{"class":143},[137,13902,13903],{"class":284}," \"..\u002F..\u002Fconfig\u002Fauth.config\"",[137,13905,3276],{"class":157},[137,13907,13908,13910,13913,13915,13918],{"class":139,"line":278},[137,13909,10287],{"class":143},[137,13911,13912],{"class":157}," { Reflector } ",[137,13914,10954],{"class":143},[137,13916,13917],{"class":284}," \"@nestjs\u002Fcore\"",[137,13919,3276],{"class":157},[137,13921,13922],{"class":139,"line":291},[137,13923,516],{"emptyLinePlaceholder":515},[137,13925,13926,13928,13930],{"class":139,"line":297},[137,13927,13382],{"class":157},[137,13929,13604],{"class":147},[137,13931,2754],{"class":157},[137,13933,13934,13936,13938,13941,13944,13947],{"class":139,"line":302},[137,13935,13456],{"class":143},[137,13937,7832],{"class":143},[137,13939,13940],{"class":147}," OktaGuard",[137,13942,13943],{"class":143}," implements",[137,13945,13946],{"class":147}," CanActivate",[137,13948,256],{"class":157},[137,13950,13951,13954,13956,13959],{"class":139,"line":662},[137,13952,13953],{"class":161},"    oktaJwtVerifier",[137,13955,894],{"class":143},[137,13957,13958],{"class":147}," OktaJwtVerifier",[137,13960,3276],{"class":157},[137,13962,13963],{"class":139,"line":667},[137,13964,516],{"emptyLinePlaceholder":515},[137,13966,13967,13969],{"class":139,"line":786},[137,13968,3651],{"class":143},[137,13970,11813],{"class":157},[137,13972,13973,13976,13979,13981,13984],{"class":139,"line":798},[137,13974,13975],{"class":143},"        private",[137,13977,13978],{"class":161}," reflector",[137,13980,894],{"class":143},[137,13982,13983],{"class":147}," Reflector",[137,13985,1961],{"class":157},[137,13987,13988,13990,13993,13996,13998],{"class":139,"line":803},[137,13989,13975],{"class":143},[137,13991,13992],{"class":143}," readonly",[137,13994,13995],{"class":161}," authConfig",[137,13997,894],{"class":143},[137,13999,14000],{"class":147}," AuthConfig\n",[137,14002,14003],{"class":139,"line":931},[137,14004,14005],{"class":157},"    ) {}\n",[137,14007,14008],{"class":139,"line":1568},[137,14009,516],{"emptyLinePlaceholder":515},[137,14011,14012,14015,14017,14020],{"class":139,"line":1573},[137,14013,14014],{"class":147},"    jwtVerifier",[137,14016,356],{"class":157},[137,14018,14019],{"class":161},"permissions",[137,14021,170],{"class":157},[137,14023,14024,14026,14029,14031,14033,14035],{"class":139,"line":1578},[137,14025,3679],{"class":364},[137,14027,14028],{"class":157},".oktaJwtVerifier ",[137,14030,253],{"class":143},[137,14032,1426],{"class":143},[137,14034,13958],{"class":147},[137,14036,3175],{"class":157},[137,14038,14039,14042,14044],{"class":139,"line":1588},[137,14040,14041],{"class":157},"            issuer: ",[137,14043,24],{"class":364},[137,14045,14046],{"class":157},".authConfig.issuer,\n",[137,14048,14049,14052,14054],{"class":139,"line":1601},[137,14050,14051],{"class":157},"            clientId: ",[137,14053,24],{"class":364},[137,14055,14056],{"class":157},".authConfig.clientId,\n",[137,14058,14059],{"class":139,"line":3802},[137,14060,14061],{"class":157},"            assertClaims: {\n",[137,14063,14064,14067],{"class":139,"line":3808},[137,14065,14066],{"class":284},"                \"permissions.includes\"",[137,14068,14069],{"class":157},": permissions,\n",[137,14071,14072],{"class":139,"line":3822},[137,14073,14074],{"class":157},"            },\n",[137,14076,14077],{"class":139,"line":3827},[137,14078,14079],{"class":157},"        });\n",[137,14081,14082],{"class":139,"line":3832},[137,14083,294],{"class":157},[137,14085,14086],{"class":139,"line":3840},[137,14087,516],{"emptyLinePlaceholder":515},[137,14089,14090,14093,14095,14098,14100,14103,14106,14108,14111,14114,14117,14119,14122,14125,14127,14130,14132,14134],{"class":139,"line":3846},[137,14091,14092],{"class":147},"    canActivate",[137,14094,356],{"class":157},[137,14096,14097],{"class":161},"context",[137,14099,894],{"class":143},[137,14101,14102],{"class":147}," ExecutionContext",[137,14104,14105],{"class":157},")",[137,14107,894],{"class":143},[137,14109,14110],{"class":364}," boolean",[137,14112,14113],{"class":143}," |",[137,14115,14116],{"class":147}," Promise",[137,14118,4033],{"class":157},[137,14120,14121],{"class":364},"boolean",[137,14123,14124],{"class":157},"> ",[137,14126,7684],{"class":143},[137,14128,14129],{"class":147}," Observable",[137,14131,4033],{"class":157},[137,14133,14121],{"class":364},[137,14135,14136],{"class":157},"> {\n",[137,14138,14139,14141,14144,14146,14148,14151,14154,14156,14159,14162,14165,14168,14171],{"class":139,"line":3861},[137,14140,3008],{"class":143},[137,14142,14143],{"class":364}," permissions",[137,14145,151],{"class":143},[137,14147,365],{"class":364},[137,14149,14150],{"class":157},".reflector.",[137,14152,14153],{"class":147},"get",[137,14155,4033],{"class":157},[137,14157,14158],{"class":364},"string",[137,14160,14161],{"class":157},"[]>(",[137,14163,14164],{"class":284},"\"permissions\"",[137,14166,14167],{"class":157},", context.",[137,14169,14170],{"class":147},"getHandler",[137,14172,14173],{"class":157},"());\n",[137,14175,14176],{"class":139,"line":3883},[137,14177,516],{"emptyLinePlaceholder":515},[137,14179,14180,14182,14184,14187],{"class":139,"line":3896},[137,14181,3679],{"class":364},[137,14183,1017],{"class":157},[137,14185,14186],{"class":147},"jwtVerifier",[137,14188,14189],{"class":157},"(permissions);\n",[137,14191,14192],{"class":139,"line":3901},[137,14193,516],{"emptyLinePlaceholder":515},[137,14195,14196,14198,14200,14202,14205,14208,14211,14213,14216,14218,14220,14222,14225,14227],{"class":139,"line":3906},[137,14197,3008],{"class":143},[137,14199,13761],{"class":364},[137,14201,151],{"class":143},[137,14203,14204],{"class":157}," context.",[137,14206,14207],{"class":147},"getArgs",[137,14209,14210],{"class":157},"()[",[137,14212,6044],{"class":364},[137,14214,14215],{"class":157},"]?.headers?.authorization.",[137,14217,8537],{"class":147},[137,14219,356],{"class":157},[137,14221,8542],{"class":284},[137,14223,14224],{"class":157},")[",[137,14226,6065],{"class":364},[137,14228,5727],{"class":157},[137,14230,14231,14233,14235],{"class":139,"line":3911},[137,14232,5472],{"class":143},[137,14234,365],{"class":364},[137,14236,14237],{"class":157},".oktaJwtVerifier\n",[137,14239,14240,14243,14246,14249,14251],{"class":139,"line":4666},[137,14241,14242],{"class":157},"            .",[137,14244,14245],{"class":147},"verifyAccessToken",[137,14247,14248],{"class":157},"(token, ",[137,14250,24],{"class":364},[137,14252,14253],{"class":157},".authConfig.audience)\n",[137,14255,14256,14258,14260,14262,14265,14267,14269],{"class":139,"line":4672},[137,14257,14242],{"class":157},[137,14259,2771],{"class":147},[137,14261,2774],{"class":157},[137,14263,14264],{"class":161},"details",[137,14266,219],{"class":157},[137,14268,222],{"class":143},[137,14270,256],{"class":157},[137,14272,14273,14275,14277],{"class":139,"line":4680},[137,14274,745],{"class":157},[137,14276,353],{"class":147},[137,14278,14279],{"class":157},"(details);\n",[137,14281,14282,14284,14287],{"class":139,"line":4711},[137,14283,5761],{"class":143},[137,14285,14286],{"class":364}," true",[137,14288,3276],{"class":157},[137,14290,14291],{"class":139,"line":4716},[137,14292,14293],{"class":157},"            })\n",[137,14295,14296,14298,14300,14302,14305,14307,14309],{"class":139,"line":4721},[137,14297,14242],{"class":157},[137,14299,2807],{"class":147},[137,14301,2774],{"class":157},[137,14303,14304],{"class":161},"err",[137,14306,219],{"class":157},[137,14308,222],{"class":143},[137,14310,256],{"class":157},[137,14312,14313,14315,14317],{"class":139,"line":4727},[137,14314,745],{"class":157},[137,14316,353],{"class":147},[137,14318,14319],{"class":157},"(err);\n",[137,14321,14322,14325,14327,14330],{"class":139,"line":4732},[137,14323,14324],{"class":143},"                throw",[137,14326,1426],{"class":143},[137,14328,14329],{"class":147}," UnauthorizedException",[137,14331,924],{"class":157},[137,14333,14334],{"class":139,"line":5006},[137,14335,14336],{"class":157},"            });\n",[137,14338,14339],{"class":139,"line":5014},[137,14340,294],{"class":157},[137,14342,14344],{"class":139,"line":14343},43,[137,14345,510],{"class":157},[27,14347,14348,14349,13801,14351,14354,14355,14358],{},"In order to use the Guard, we will first need to declare our custom decorator. Create a new directory inside the ",[22,14350,13088],{},[22,14352,14353],{},"decorators\u002F",". Inside the directory, create a file ",[22,14356,14357],{},"auth.decorator.ts"," where we can declare our custom decorator.",[128,14360,14362],{"className":13299,"code":14361,"language":13301,"meta":133,"style":133},"import { applyDecorators, UseGuards, SetMetadata } from \"@nestjs\u002Fcommon\";\nimport { OktaGuard } from \"..\u002Fguards\u002Fauth.guard\";\n\nexport function Auth(...permissions: string[]) {\n    return applyDecorators(SetMetadata(\"permissions\", permissions), UseGuards(OktaGuard));\n}\n",[22,14363,14364,14377,14391,14395,14418,14443],{"__ignoreMap":133},[137,14365,14366,14368,14371,14373,14375],{"class":139,"line":140},[137,14367,10287],{"class":143},[137,14369,14370],{"class":157}," { applyDecorators, UseGuards, SetMetadata } ",[137,14372,10954],{"class":143},[137,14374,13315],{"class":284},[137,14376,3276],{"class":157},[137,14378,14379,14381,14384,14386,14389],{"class":139,"line":173},[137,14380,10287],{"class":143},[137,14382,14383],{"class":157}," { OktaGuard } ",[137,14385,10954],{"class":143},[137,14387,14388],{"class":284}," \"..\u002Fguards\u002Fauth.guard\"",[137,14390,3276],{"class":157},[137,14392,14393],{"class":139,"line":188},[137,14394,516],{"emptyLinePlaceholder":515},[137,14396,14397,14399,14401,14404,14406,14409,14411,14413,14415],{"class":139,"line":269},[137,14398,13456],{"class":143},[137,14400,154],{"class":143},[137,14402,14403],{"class":147}," Auth",[137,14405,356],{"class":157},[137,14407,14408],{"class":143},"...",[137,14410,14019],{"class":161},[137,14412,894],{"class":143},[137,14414,13630],{"class":364},[137,14416,14417],{"class":157},"[]) {\n",[137,14419,14420,14422,14425,14427,14430,14432,14434,14437,14440],{"class":139,"line":278},[137,14421,176],{"class":143},[137,14423,14424],{"class":147}," applyDecorators",[137,14426,356],{"class":157},[137,14428,14429],{"class":147},"SetMetadata",[137,14431,356],{"class":157},[137,14433,14164],{"class":284},[137,14435,14436],{"class":157},", permissions), ",[137,14438,14439],{"class":147},"UseGuards",[137,14441,14442],{"class":157},"(OktaGuard));\n",[137,14444,14445],{"class":139,"line":291},[137,14446,510],{"class":157},[27,14448,14449],{},"And that's it. I will demonstrate how we can use this custom decorator to protect our endpoints later in this article.",[27,14451,14452],{},"We will now implement the user creation using the Okta Node.js SDK.",[104,14454,14456],{"id":14455},"user-creation-with-okta-nodejs-sdk","User Creation with Okta Node.js SDK",[27,14458,14459,14460,14465],{},"We will first create users with specific permissions. The user creation will be handled by the Okta Node.js SDK. Let's first install the ",[45,14461,14464],{"href":14462,"target":2716,"rel":14463},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@okta\u002Fokta-sdk-nodejs",[2718,2719],"Okta Node.js SDK"," with the following command.",[128,14467,14469],{"className":8665,"code":14468,"language":8667,"meta":133,"style":133},"npm install @okta\u002Fokta-sdk-nodejs\n",[22,14470,14471],{"__ignoreMap":133},[137,14472,14473,14475,14477],{"class":139,"line":140},[137,14474,9536],{"class":147},[137,14476,10268],{"class":284},[137,14478,14479],{"class":284}," @okta\u002Fokta-sdk-nodejs\n",[27,14481,14482],{},"Before we jump to the implementation on the user creation, a couple of things needs to be done beforehand. First, extend the user profile in the Okta dashboard with one additional attribute that is going to be an array of permissions. Secondly, create the Data Transfer Object (DTO) file where will define the user properties that will be validated on incoming requests.",[123,14484,14486],{"id":14485},"add-a-new-attribute-to-the-user-profile","Add a new Attribute to the user profile",[27,14488,14489,14490,14493],{},"To add a ",[22,14491,14492],{},"Permission"," attribute, navigate to Discovery → Profile Editor → User (default) → Add Attribute in the Okta dashboard.",[27,14495,14496],{},[63,14497],{"alt":14498,"src":14499},"Adding Permission attribute to Okta.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1664764468\u002Fblog\u002Fnestjs-auth-authorisation-with-okta\u002Fnestjs-auth-authorisation-with-okta_muplt4",[27,14501,14502],{},"In the dialog, select the following data: Data type to be array of strings, Display name: Permissions, Variable name: permissions, Description: Users Permissions, Attribute required: selected.",[27,14504,14505],{},[63,14506],{"alt":14507,"src":14508},"Setting up Permission attribute to Okta.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1664776160\u002Fblog\u002Fnestjs-auth-authorisation-with-okta\u002Fnestjs-auth-authorisation-with-okta_pp9nnk",[27,14510,14511],{},"Upon saving the above attributes, our new attribute will be added to the list of attributes.",[27,14513,14514],{},[63,14515],{"alt":14516,"src":14517},"New attribute will be added to the list of attributes.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1664776285\u002Fblog\u002Fnestjs-auth-authorisation-with-okta\u002Fnestjs-auth-authorisation-with-okta_xxbonx",[123,14519,14521],{"id":14520},"create-a-dto","Create a DTO",[27,14523,14524,14525,14528,14529,13818],{},"We will now create a DTO inside our user module. Install two more libraries that will help us with the incoming request validation. We can do so by installing ",[22,14526,14527],{},"class-validator",", and ",[22,14530,14531],{},"class-transformer",[128,14533,14535],{"className":8665,"code":14534,"language":8667,"meta":133,"style":133},"npm install class-validator class-transformer --save\n",[22,14536,14537],{"__ignoreMap":133},[137,14538,14539,14541,14543,14546,14549],{"class":139,"line":140},[137,14540,9536],{"class":147},[137,14542,10268],{"class":284},[137,14544,14545],{"class":284}," class-validator",[137,14547,14548],{"class":284}," class-transformer",[137,14550,14551],{"class":364}," --save\n",[27,14553,14554,14555,9335,14558,14561,14562,14564,14565,14568,14569,14571],{},"Next, create a new directory called ",[22,14556,14557],{},"dto\u002F",[22,14559,14560],{},"src\u002Fuser\u002F",". Inside ",[22,14563,14557],{}," create ",[22,14566,14567],{},"user.dto.ts"," file. The code in ",[22,14570,14567],{}," file will look like this:",[128,14573,14575],{"className":13299,"code":14574,"language":13301,"meta":133,"style":133},"import { IsEmail, IsNotEmpty, MaxLength, MinLength, Matches, IsEnum, IsAlpha } from \"class-validator\";\n\nenum Permissions {\n    ADMIN = \"ADMIN\",\n    USER = \"USER\",\n    DEVELOPER = \"DEVELOPER\",\n}\n\nexport class UserDto {\n    @IsEmail()\n    @IsNotEmpty()\n    email: string;\n\n    @IsNotEmpty()\n    @MinLength(8)\n    @MaxLength(20)\n    @Matches(\u002F^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*]).{8,20}$\u002F, {\n        message: \"password too weak\",\n    })\n    password: string;\n\n    @IsNotEmpty()\n    @MinLength(2)\n    @MaxLength(20)\n    @IsAlpha()\n    firstName: string;\n\n    @IsNotEmpty()\n    @MinLength(2)\n    @MaxLength(20)\n    @IsAlpha()\n    lastName: string;\n\n    @IsNotEmpty()\n    @IsEnum(Permissions, { each: true })\n    permissions: Permissions[];\n}\n",[22,14576,14577,14591,14595,14605,14617,14629,14641,14645,14649,14660,14670,14679,14690,14694,14702,14716,14730,14794,14804,14808,14819,14823,14831,14843,14855,14864,14875,14879,14887,14899,14911,14919,14930,14934,14942,14957,14969],{"__ignoreMap":133},[137,14578,14579,14581,14584,14586,14589],{"class":139,"line":140},[137,14580,10287],{"class":143},[137,14582,14583],{"class":157}," { IsEmail, IsNotEmpty, MaxLength, MinLength, Matches, IsEnum, IsAlpha } ",[137,14585,10954],{"class":143},[137,14587,14588],{"class":284}," \"class-validator\"",[137,14590,3276],{"class":157},[137,14592,14593],{"class":139,"line":173},[137,14594,516],{"emptyLinePlaceholder":515},[137,14596,14597,14600,14603],{"class":139,"line":188},[137,14598,14599],{"class":143},"enum",[137,14601,14602],{"class":147}," Permissions",[137,14604,256],{"class":157},[137,14606,14607,14610,14612,14615],{"class":139,"line":269},[137,14608,14609],{"class":364},"    ADMIN",[137,14611,151],{"class":143},[137,14613,14614],{"class":284}," \"ADMIN\"",[137,14616,1961],{"class":157},[137,14618,14619,14622,14624,14627],{"class":139,"line":278},[137,14620,14621],{"class":364},"    USER",[137,14623,151],{"class":143},[137,14625,14626],{"class":284}," \"USER\"",[137,14628,1961],{"class":157},[137,14630,14631,14634,14636,14639],{"class":139,"line":291},[137,14632,14633],{"class":364},"    DEVELOPER",[137,14635,151],{"class":143},[137,14637,14638],{"class":284}," \"DEVELOPER\"",[137,14640,1961],{"class":157},[137,14642,14643],{"class":139,"line":297},[137,14644,510],{"class":157},[137,14646,14647],{"class":139,"line":302},[137,14648,516],{"emptyLinePlaceholder":515},[137,14650,14651,14653,14655,14658],{"class":139,"line":662},[137,14652,13456],{"class":143},[137,14654,7832],{"class":143},[137,14656,14657],{"class":147}," UserDto",[137,14659,256],{"class":157},[137,14661,14662,14665,14668],{"class":139,"line":667},[137,14663,14664],{"class":157},"    @",[137,14666,14667],{"class":147},"IsEmail",[137,14669,2754],{"class":157},[137,14671,14672,14674,14677],{"class":139,"line":786},[137,14673,14664],{"class":157},[137,14675,14676],{"class":147},"IsNotEmpty",[137,14678,2754],{"class":157},[137,14680,14681,14684,14686,14688],{"class":139,"line":798},[137,14682,14683],{"class":161},"    email",[137,14685,894],{"class":143},[137,14687,13630],{"class":364},[137,14689,3276],{"class":157},[137,14691,14692],{"class":139,"line":803},[137,14693,516],{"emptyLinePlaceholder":515},[137,14695,14696,14698,14700],{"class":139,"line":931},[137,14697,14664],{"class":157},[137,14699,14676],{"class":147},[137,14701,2754],{"class":157},[137,14703,14704,14706,14709,14711,14714],{"class":139,"line":1568},[137,14705,14664],{"class":157},[137,14707,14708],{"class":147},"MinLength",[137,14710,356],{"class":157},[137,14712,14713],{"class":364},"8",[137,14715,3155],{"class":157},[137,14717,14718,14720,14723,14725,14728],{"class":139,"line":1573},[137,14719,14664],{"class":157},[137,14721,14722],{"class":147},"MaxLength",[137,14724,356],{"class":157},[137,14726,14727],{"class":364},"20",[137,14729,3155],{"class":157},[137,14731,14732,14734,14737,14739,14741,14744,14748,14750,14752,14755,14758,14760,14762,14765,14767,14769,14771,14774,14776,14778,14780,14783,14785,14787,14790,14792],{"class":139,"line":1578},[137,14733,14664],{"class":157},[137,14735,14736],{"class":147},"Matches",[137,14738,356],{"class":157},[137,14740,47],{"class":284},[137,14742,14743],{"class":143},"^",[137,14745,14747],{"class":14746},"sA_wV","(?=",[137,14749,1017],{"class":364},[137,14751,7672],{"class":143},[137,14753,14754],{"class":364},"\\d",[137,14756,14757],{"class":14746},")(?=",[137,14759,1017],{"class":364},[137,14761,7672],{"class":143},[137,14763,14764],{"class":364},"[a-z]",[137,14766,14757],{"class":14746},[137,14768,1017],{"class":364},[137,14770,7672],{"class":143},[137,14772,14773],{"class":364},"[A-Z]",[137,14775,14757],{"class":14746},[137,14777,1017],{"class":364},[137,14779,7672],{"class":143},[137,14781,14782],{"class":364},"[!@#$%^&*]",[137,14784,14105],{"class":14746},[137,14786,1017],{"class":364},[137,14788,14789],{"class":143},"{8,20}$",[137,14791,47],{"class":284},[137,14793,5396],{"class":157},[137,14795,14796,14799,14802],{"class":139,"line":1588},[137,14797,14798],{"class":157},"        message: ",[137,14800,14801],{"class":284},"\"password too weak\"",[137,14803,1961],{"class":157},[137,14805,14806],{"class":139,"line":1601},[137,14807,2800],{"class":157},[137,14809,14810,14813,14815,14817],{"class":139,"line":3802},[137,14811,14812],{"class":161},"    password",[137,14814,894],{"class":143},[137,14816,13630],{"class":364},[137,14818,3276],{"class":157},[137,14820,14821],{"class":139,"line":3808},[137,14822,516],{"emptyLinePlaceholder":515},[137,14824,14825,14827,14829],{"class":139,"line":3822},[137,14826,14664],{"class":157},[137,14828,14676],{"class":147},[137,14830,2754],{"class":157},[137,14832,14833,14835,14837,14839,14841],{"class":139,"line":3827},[137,14834,14664],{"class":157},[137,14836,14708],{"class":147},[137,14838,356],{"class":157},[137,14840,10345],{"class":364},[137,14842,3155],{"class":157},[137,14844,14845,14847,14849,14851,14853],{"class":139,"line":3832},[137,14846,14664],{"class":157},[137,14848,14722],{"class":147},[137,14850,356],{"class":157},[137,14852,14727],{"class":364},[137,14854,3155],{"class":157},[137,14856,14857,14859,14862],{"class":139,"line":3840},[137,14858,14664],{"class":157},[137,14860,14861],{"class":147},"IsAlpha",[137,14863,2754],{"class":157},[137,14865,14866,14869,14871,14873],{"class":139,"line":3846},[137,14867,14868],{"class":161},"    firstName",[137,14870,894],{"class":143},[137,14872,13630],{"class":364},[137,14874,3276],{"class":157},[137,14876,14877],{"class":139,"line":3861},[137,14878,516],{"emptyLinePlaceholder":515},[137,14880,14881,14883,14885],{"class":139,"line":3883},[137,14882,14664],{"class":157},[137,14884,14676],{"class":147},[137,14886,2754],{"class":157},[137,14888,14889,14891,14893,14895,14897],{"class":139,"line":3896},[137,14890,14664],{"class":157},[137,14892,14708],{"class":147},[137,14894,356],{"class":157},[137,14896,10345],{"class":364},[137,14898,3155],{"class":157},[137,14900,14901,14903,14905,14907,14909],{"class":139,"line":3901},[137,14902,14664],{"class":157},[137,14904,14722],{"class":147},[137,14906,356],{"class":157},[137,14908,14727],{"class":364},[137,14910,3155],{"class":157},[137,14912,14913,14915,14917],{"class":139,"line":3906},[137,14914,14664],{"class":157},[137,14916,14861],{"class":147},[137,14918,2754],{"class":157},[137,14920,14921,14924,14926,14928],{"class":139,"line":3911},[137,14922,14923],{"class":161},"    lastName",[137,14925,894],{"class":143},[137,14927,13630],{"class":364},[137,14929,3276],{"class":157},[137,14931,14932],{"class":139,"line":4666},[137,14933,516],{"emptyLinePlaceholder":515},[137,14935,14936,14938,14940],{"class":139,"line":4672},[137,14937,14664],{"class":157},[137,14939,14676],{"class":147},[137,14941,2754],{"class":157},[137,14943,14944,14946,14949,14952,14954],{"class":139,"line":4680},[137,14945,14664],{"class":157},[137,14947,14948],{"class":147},"IsEnum",[137,14950,14951],{"class":157},"(Permissions, { each: ",[137,14953,3097],{"class":364},[137,14955,14956],{"class":157}," })\n",[137,14958,14959,14962,14964,14966],{"class":139,"line":4711},[137,14960,14961],{"class":161},"    permissions",[137,14963,894],{"class":143},[137,14965,14602],{"class":147},[137,14967,14968],{"class":157},"[];\n",[137,14970,14971],{"class":139,"line":4716},[137,14972,510],{"class":157},[27,14974,14975,14976,164,14979,114,14982,1017],{},"We should expect five properties to be sent with the request: an email address, a strong password, users first and last name and user's permissions (roles) which will accept three possible values, ",[22,14977,14978],{},"ADMIN",[22,14980,14981],{},"USER",[22,14983,14984],{},"DEVELOPER",[27,14986,14987,14988,2107,14991,14993,14994,14996],{},"One more thing needs to be done to make the DTO work. We need to import and set the ",[22,14989,14990],{},"ValidationPipe",[22,14992,13114],{}," file. Our ",[22,14995,13114],{}," file should look like this:",[128,14998,15000],{"className":13299,"code":14999,"language":13301,"meta":133,"style":133},"import { NestFactory } from \"@nestjs\u002Fcore\";\nimport { AppModule } from \".\u002Fapp.module\";\nimport { ValidationPipe } from \"@nestjs\u002Fcommon\";\n\nasync function bootstrap() {\n    const app = await NestFactory.create(AppModule);\n    app.useGlobalPipes(new ValidationPipe());\n    await app.listen(3000);\n}\nbootstrap();\n",[22,15001,15002,15015,15029,15042,15046,15058,15079,15096,15114,15118],{"__ignoreMap":133},[137,15003,15004,15006,15009,15011,15013],{"class":139,"line":140},[137,15005,10287],{"class":143},[137,15007,15008],{"class":157}," { NestFactory } ",[137,15010,10954],{"class":143},[137,15012,13917],{"class":284},[137,15014,3276],{"class":157},[137,15016,15017,15019,15022,15024,15027],{"class":139,"line":173},[137,15018,10287],{"class":143},[137,15020,15021],{"class":157}," { AppModule } ",[137,15023,10954],{"class":143},[137,15025,15026],{"class":284}," \".\u002Fapp.module\"",[137,15028,3276],{"class":157},[137,15030,15031,15033,15036,15038,15040],{"class":139,"line":188},[137,15032,10287],{"class":143},[137,15034,15035],{"class":157}," { ValidationPipe } ",[137,15037,10954],{"class":143},[137,15039,13315],{"class":284},[137,15041,3276],{"class":157},[137,15043,15044],{"class":139,"line":269},[137,15045,516],{"emptyLinePlaceholder":515},[137,15047,15048,15051,15053,15056],{"class":139,"line":278},[137,15049,15050],{"class":143},"async",[137,15052,154],{"class":143},[137,15054,15055],{"class":147}," bootstrap",[137,15057,275],{"class":157},[137,15059,15060,15062,15065,15067,15070,15073,15076],{"class":139,"line":291},[137,15061,4177],{"class":143},[137,15063,15064],{"class":364}," app",[137,15066,151],{"class":143},[137,15068,15069],{"class":143}," await",[137,15071,15072],{"class":157}," NestFactory.",[137,15074,15075],{"class":147},"create",[137,15077,15078],{"class":157},"(AppModule);\n",[137,15080,15081,15084,15087,15089,15091,15094],{"class":139,"line":297},[137,15082,15083],{"class":157},"    app.",[137,15085,15086],{"class":147},"useGlobalPipes",[137,15088,356],{"class":157},[137,15090,1361],{"class":143},[137,15092,15093],{"class":147}," ValidationPipe",[137,15095,14173],{"class":157},[137,15097,15098,15101,15104,15107,15109,15112],{"class":139,"line":302},[137,15099,15100],{"class":143},"    await",[137,15102,15103],{"class":157}," app.",[137,15105,15106],{"class":147},"listen",[137,15108,356],{"class":157},[137,15110,15111],{"class":364},"3000",[137,15113,1502],{"class":157},[137,15115,15116],{"class":139,"line":662},[137,15117,510],{"class":157},[137,15119,15120,15123],{"class":139,"line":667},[137,15121,15122],{"class":147},"bootstrap",[137,15124,924],{"class":157},[123,15126,15128],{"id":15127},"user-creation","User Creation",[27,15130,15131,15132,15135],{},"Before jumping on the user's sign up implementation, we need to setup the ",[45,15133,14464],{"href":14462,"target":2716,"rel":15134},[2718,2719],". The Okta Node.js SDK can be installed with the following command:",[128,15137,15138],{"className":8665,"code":14468,"language":8667,"meta":133,"style":133},[22,15139,15140],{"__ignoreMap":133},[137,15141,15142,15144,15146],{"class":139,"line":140},[137,15143,9536],{"class":147},[137,15145,10268],{"class":284},[137,15147,14479],{"class":284},[27,15149,15150,15151,15154,15155,15157],{},"Next, create a file called ",[22,15152,15153],{},"okta.setup.ts"," inside the user's module ",[22,15156,14560],{},". Define the Okta Node.js SDK inside that file as follows:",[128,15159,15161],{"className":13299,"code":15160,"language":13301,"meta":133,"style":133},"import { Injectable } from \"@nestjs\u002Fcommon\";\nimport { OktaSdkConfig } from \"..\u002F..\u002Fconfig\u002Fokta.sdk.config\";\nimport { Client } from \"@okta\u002Fokta-sdk-nodejs\";\n\n@Injectable()\nexport class Okta {\n    constructor(private readonly oktaSdkConfig: OktaSdkConfig) {}\n\n    setup() {\n        const client = new Client({\n            orgUrl: this.oktaSdkConfig.orgUrl,\n            token: this.oktaSdkConfig.token,\n        });\n        return client;\n    }\n}\n",[22,15162,15163,15175,15189,15203,15207,15215,15226,15247,15251,15258,15274,15284,15294,15298,15305,15309],{"__ignoreMap":133},[137,15164,15165,15167,15169,15171,15173],{"class":139,"line":140},[137,15166,10287],{"class":143},[137,15168,13587],{"class":157},[137,15170,10954],{"class":143},[137,15172,13315],{"class":284},[137,15174,3276],{"class":157},[137,15176,15177,15179,15182,15184,15187],{"class":139,"line":173},[137,15178,10287],{"class":143},[137,15180,15181],{"class":157}," { OktaSdkConfig } ",[137,15183,10954],{"class":143},[137,15185,15186],{"class":284}," \"..\u002F..\u002Fconfig\u002Fokta.sdk.config\"",[137,15188,3276],{"class":157},[137,15190,15191,15193,15196,15198,15201],{"class":139,"line":188},[137,15192,10287],{"class":143},[137,15194,15195],{"class":157}," { Client } ",[137,15197,10954],{"class":143},[137,15199,15200],{"class":284}," \"@okta\u002Fokta-sdk-nodejs\"",[137,15202,3276],{"class":157},[137,15204,15205],{"class":139,"line":269},[137,15206,516],{"emptyLinePlaceholder":515},[137,15208,15209,15211,15213],{"class":139,"line":278},[137,15210,13382],{"class":157},[137,15212,13604],{"class":147},[137,15214,2754],{"class":157},[137,15216,15217,15219,15221,15224],{"class":139,"line":291},[137,15218,13456],{"class":143},[137,15220,7832],{"class":143},[137,15222,15223],{"class":147}," Okta",[137,15225,256],{"class":157},[137,15227,15228,15230,15232,15235,15237,15240,15242,15244],{"class":139,"line":297},[137,15229,3651],{"class":143},[137,15231,356],{"class":157},[137,15233,15234],{"class":143},"private",[137,15236,13992],{"class":143},[137,15238,15239],{"class":161}," oktaSdkConfig",[137,15241,894],{"class":143},[137,15243,13733],{"class":147},[137,15245,15246],{"class":157},") {}\n",[137,15248,15249],{"class":139,"line":302},[137,15250,516],{"emptyLinePlaceholder":515},[137,15252,15253,15256],{"class":139,"line":662},[137,15254,15255],{"class":147},"    setup",[137,15257,275],{"class":157},[137,15259,15260,15262,15265,15267,15269,15272],{"class":139,"line":667},[137,15261,3008],{"class":143},[137,15263,15264],{"class":364}," client",[137,15266,151],{"class":143},[137,15268,1426],{"class":143},[137,15270,15271],{"class":147}," Client",[137,15273,3175],{"class":157},[137,15275,15276,15279,15281],{"class":139,"line":786},[137,15277,15278],{"class":157},"            orgUrl: ",[137,15280,24],{"class":364},[137,15282,15283],{"class":157},".oktaSdkConfig.orgUrl,\n",[137,15285,15286,15289,15291],{"class":139,"line":798},[137,15287,15288],{"class":157},"            token: ",[137,15290,24],{"class":364},[137,15292,15293],{"class":157},".oktaSdkConfig.token,\n",[137,15295,15296],{"class":139,"line":803},[137,15297,14079],{"class":157},[137,15299,15300,15302],{"class":139,"line":931},[137,15301,5472],{"class":143},[137,15303,15304],{"class":157}," client;\n",[137,15306,15307],{"class":139,"line":1568},[137,15308,294],{"class":157},[137,15310,15311],{"class":139,"line":1573},[137,15312,510],{"class":157},[27,15314,15315,15316,14996],{},"To be able to use the setup above, declare it as a provider in our user module. Our ",[22,15317,13189],{},[128,15319,15321],{"className":13299,"code":15320,"language":13301,"meta":133,"style":133},"import { Module } from \"@nestjs\u002Fcommon\";\nimport { UserService } from \".\u002Fuser.service\";\nimport { UserController } from \".\u002Fuser.controller\";\nimport { OktaSdkConfig } from \"config\u002Fokta.sdk.config\";\nimport { Okta } from \".\u002Fokta.setup\";\n\n@Module({\n    imports: [],\n    providers: [UserService, Okta, OktaSdkConfig],\n    controllers: [UserController],\n    exports: [UserService],\n})\nexport class UserModule {}\n",[22,15322,15323,15335,15349,15363,15376,15390,15394,15402,15407,15412,15417,15422,15426],{"__ignoreMap":133},[137,15324,15325,15327,15329,15331,15333],{"class":139,"line":140},[137,15326,10287],{"class":143},[137,15328,13310],{"class":157},[137,15330,10954],{"class":143},[137,15332,13315],{"class":284},[137,15334,3276],{"class":157},[137,15336,15337,15339,15342,15344,15347],{"class":139,"line":173},[137,15338,10287],{"class":143},[137,15340,15341],{"class":157}," { UserService } ",[137,15343,10954],{"class":143},[137,15345,15346],{"class":284}," \".\u002Fuser.service\"",[137,15348,3276],{"class":157},[137,15350,15351,15353,15356,15358,15361],{"class":139,"line":188},[137,15352,10287],{"class":143},[137,15354,15355],{"class":157}," { UserController } ",[137,15357,10954],{"class":143},[137,15359,15360],{"class":284}," \".\u002Fuser.controller\"",[137,15362,3276],{"class":157},[137,15364,15365,15367,15369,15371,15374],{"class":139,"line":269},[137,15366,10287],{"class":143},[137,15368,15181],{"class":157},[137,15370,10954],{"class":143},[137,15372,15373],{"class":284}," \"config\u002Fokta.sdk.config\"",[137,15375,3276],{"class":157},[137,15377,15378,15380,15383,15385,15388],{"class":139,"line":278},[137,15379,10287],{"class":143},[137,15381,15382],{"class":157}," { Okta } ",[137,15384,10954],{"class":143},[137,15386,15387],{"class":284}," \".\u002Fokta.setup\"",[137,15389,3276],{"class":157},[137,15391,15392],{"class":139,"line":291},[137,15393,516],{"emptyLinePlaceholder":515},[137,15395,15396,15398,15400],{"class":139,"line":297},[137,15397,13382],{"class":157},[137,15399,13385],{"class":147},[137,15401,3175],{"class":157},[137,15403,15404],{"class":139,"line":302},[137,15405,15406],{"class":157},"    imports: [],\n",[137,15408,15409],{"class":139,"line":662},[137,15410,15411],{"class":157},"    providers: [UserService, Okta, OktaSdkConfig],\n",[137,15413,15414],{"class":139,"line":667},[137,15415,15416],{"class":157},"    controllers: [UserController],\n",[137,15418,15419],{"class":139,"line":786},[137,15420,15421],{"class":157},"    exports: [UserService],\n",[137,15423,15424],{"class":139,"line":798},[137,15425,13451],{"class":157},[137,15427,15428,15430,15432,15435],{"class":139,"line":803},[137,15429,13456],{"class":143},[137,15431,7832],{"class":143},[137,15433,15434],{"class":147}," UserModule",[137,15436,13464],{"class":157},[27,15438,15439,15440,15443,15444,15447,15448,15451],{},"We can now use Okta setup in the user's service. Head over to the user's service file and create a method called ",[22,15441,15442],{},"createUser"," inside the ",[22,15445,15446],{},"UserService"," class. The ",[22,15449,15450],{},"user.service.ts"," file should appear as follows:",[128,15453,15455],{"className":13299,"code":15454,"language":13301,"meta":133,"style":133},"import { BadRequestException, Injectable } from \"@nestjs\u002Fcommon\";\nimport { UserDto } from \".\u002Fdto\u002Fuser.dto\";\nimport { Okta } from \".\u002Fokta.setup\";\n\n@Injectable()\nexport class UserService {\n    constructor(private readonly okta: Okta) {}\n\n    async createUser(userRequest: UserDto) {\n        const { email, password, firstName, lastName, permissions } = userRequest;\n        const okta = this.okta.setup();\n\n        const newUser = {\n            profile: {\n                email: email,\n                login: email,\n                firstName: firstName,\n                lastName: lastName,\n                permissions: permissions,\n            },\n            credentials: {\n                password: {\n                    value: password,\n                },\n            },\n        };\n\n        try {\n            const user = await okta.createUser(newUser);\n            return user;\n        } catch (error) {\n            throw new BadRequestException(error.message);\n        }\n    }\n}\n",[22,15456,15457,15470,15484,15496,15500,15508,15519,15538,15542,15561,15594,15612,15616,15627,15632,15637,15642,15647,15652,15657,15661,15666,15671,15676,15681,15685,15689,15693,15700,15718,15725,15735,15748,15752,15756],{"__ignoreMap":133},[137,15458,15459,15461,15464,15466,15468],{"class":139,"line":140},[137,15460,10287],{"class":143},[137,15462,15463],{"class":157}," { BadRequestException, Injectable } ",[137,15465,10954],{"class":143},[137,15467,13315],{"class":284},[137,15469,3276],{"class":157},[137,15471,15472,15474,15477,15479,15482],{"class":139,"line":173},[137,15473,10287],{"class":143},[137,15475,15476],{"class":157}," { UserDto } ",[137,15478,10954],{"class":143},[137,15480,15481],{"class":284}," \".\u002Fdto\u002Fuser.dto\"",[137,15483,3276],{"class":157},[137,15485,15486,15488,15490,15492,15494],{"class":139,"line":188},[137,15487,10287],{"class":143},[137,15489,15382],{"class":157},[137,15491,10954],{"class":143},[137,15493,15387],{"class":284},[137,15495,3276],{"class":157},[137,15497,15498],{"class":139,"line":269},[137,15499,516],{"emptyLinePlaceholder":515},[137,15501,15502,15504,15506],{"class":139,"line":278},[137,15503,13382],{"class":157},[137,15505,13604],{"class":147},[137,15507,2754],{"class":157},[137,15509,15510,15512,15514,15517],{"class":139,"line":291},[137,15511,13456],{"class":143},[137,15513,7832],{"class":143},[137,15515,15516],{"class":147}," UserService",[137,15518,256],{"class":157},[137,15520,15521,15523,15525,15527,15529,15532,15534,15536],{"class":139,"line":297},[137,15522,3651],{"class":143},[137,15524,356],{"class":157},[137,15526,15234],{"class":143},[137,15528,13992],{"class":143},[137,15530,15531],{"class":161}," okta",[137,15533,894],{"class":143},[137,15535,15223],{"class":147},[137,15537,15246],{"class":157},[137,15539,15540],{"class":139,"line":302},[137,15541,516],{"emptyLinePlaceholder":515},[137,15543,15544,15547,15550,15552,15555,15557,15559],{"class":139,"line":662},[137,15545,15546],{"class":143},"    async",[137,15548,15549],{"class":147}," createUser",[137,15551,356],{"class":157},[137,15553,15554],{"class":161},"userRequest",[137,15556,894],{"class":143},[137,15558,14657],{"class":147},[137,15560,170],{"class":157},[137,15562,15563,15565,15567,15570,15572,15575,15577,15579,15581,15583,15585,15587,15589,15591],{"class":139,"line":667},[137,15564,3008],{"class":143},[137,15566,8906],{"class":157},[137,15568,15569],{"class":364},"email",[137,15571,164],{"class":157},[137,15573,15574],{"class":364},"password",[137,15576,164],{"class":157},[137,15578,4693],{"class":364},[137,15580,164],{"class":157},[137,15582,4703],{"class":364},[137,15584,164],{"class":157},[137,15586,14019],{"class":364},[137,15588,8911],{"class":157},[137,15590,253],{"class":143},[137,15592,15593],{"class":157}," userRequest;\n",[137,15595,15596,15598,15600,15602,15604,15607,15610],{"class":139,"line":786},[137,15597,3008],{"class":143},[137,15599,15531],{"class":364},[137,15601,151],{"class":143},[137,15603,365],{"class":364},[137,15605,15606],{"class":157},".okta.",[137,15608,15609],{"class":147},"setup",[137,15611,924],{"class":157},[137,15613,15614],{"class":139,"line":798},[137,15615,516],{"emptyLinePlaceholder":515},[137,15617,15618,15620,15623,15625],{"class":139,"line":803},[137,15619,3008],{"class":143},[137,15621,15622],{"class":364}," newUser",[137,15624,151],{"class":143},[137,15626,256],{"class":157},[137,15628,15629],{"class":139,"line":931},[137,15630,15631],{"class":157},"            profile: {\n",[137,15633,15634],{"class":139,"line":1568},[137,15635,15636],{"class":157},"                email: email,\n",[137,15638,15639],{"class":139,"line":1573},[137,15640,15641],{"class":157},"                login: email,\n",[137,15643,15644],{"class":139,"line":1578},[137,15645,15646],{"class":157},"                firstName: firstName,\n",[137,15648,15649],{"class":139,"line":1588},[137,15650,15651],{"class":157},"                lastName: lastName,\n",[137,15653,15654],{"class":139,"line":1601},[137,15655,15656],{"class":157},"                permissions: permissions,\n",[137,15658,15659],{"class":139,"line":3802},[137,15660,14074],{"class":157},[137,15662,15663],{"class":139,"line":3808},[137,15664,15665],{"class":157},"            credentials: {\n",[137,15667,15668],{"class":139,"line":3822},[137,15669,15670],{"class":157},"                password: {\n",[137,15672,15673],{"class":139,"line":3827},[137,15674,15675],{"class":157},"                    value: password,\n",[137,15677,15678],{"class":139,"line":3832},[137,15679,15680],{"class":157},"                },\n",[137,15682,15683],{"class":139,"line":3840},[137,15684,14074],{"class":157},[137,15686,15687],{"class":139,"line":3846},[137,15688,1507],{"class":157},[137,15690,15691],{"class":139,"line":3861},[137,15692,516],{"emptyLinePlaceholder":515},[137,15694,15695,15698],{"class":139,"line":3883},[137,15696,15697],{"class":143},"        try",[137,15699,256],{"class":157},[137,15701,15702,15704,15706,15708,15710,15713,15715],{"class":139,"line":3896},[137,15703,5772],{"class":143},[137,15705,13217],{"class":364},[137,15707,151],{"class":143},[137,15709,15069],{"class":143},[137,15711,15712],{"class":157}," okta.",[137,15714,15442],{"class":147},[137,15716,15717],{"class":157},"(newUser);\n",[137,15719,15720,15722],{"class":139,"line":3901},[137,15721,4683],{"class":143},[137,15723,15724],{"class":157}," user;\n",[137,15726,15727,15730,15732],{"class":139,"line":3906},[137,15728,15729],{"class":157},"        } ",[137,15731,2807],{"class":143},[137,15733,15734],{"class":157}," (error) {\n",[137,15736,15737,15740,15742,15745],{"class":139,"line":3911},[137,15738,15739],{"class":143},"            throw",[137,15741,1426],{"class":143},[137,15743,15744],{"class":147}," BadRequestException",[137,15746,15747],{"class":157},"(error.message);\n",[137,15749,15750],{"class":139,"line":4666},[137,15751,1966],{"class":157},[137,15753,15754],{"class":139,"line":4672},[137,15755,294],{"class":157},[137,15757,15758],{"class":139,"line":4680},[137,15759,510],{"class":157},[27,15761,15762,15763,14996],{},"We can make a use of this module inside user's controller where we will now declare our endpoint. Our ",[22,15764,15765],{},"user.controller.ts",[128,15767,15769],{"className":13299,"code":15768,"language":13301,"meta":133,"style":133},"import { Controller, Post, Body } from \"@nestjs\u002Fcommon\";\nimport { UserService } from \".\u002Fuser.service\";\nimport { UserDto } from \".\u002Fdto\u002Fuser.dto\";\n\n@Controller(\"user\")\nexport class UserController {\n    constructor(private readonly userService: UserService) {}\n\n    @Post(\"\u002Fsignup\")\n    signup(@Body() userRequest: UserDto) {\n        return this.userService.createUser(userRequest);\n    }\n}\n",[22,15770,15771,15784,15796,15808,15812,15826,15837,15856,15860,15874,15895,15909,15913],{"__ignoreMap":133},[137,15772,15773,15775,15778,15780,15782],{"class":139,"line":140},[137,15774,10287],{"class":143},[137,15776,15777],{"class":157}," { Controller, Post, Body } ",[137,15779,10954],{"class":143},[137,15781,13315],{"class":284},[137,15783,3276],{"class":157},[137,15785,15786,15788,15790,15792,15794],{"class":139,"line":173},[137,15787,10287],{"class":143},[137,15789,15341],{"class":157},[137,15791,10954],{"class":143},[137,15793,15346],{"class":284},[137,15795,3276],{"class":157},[137,15797,15798,15800,15802,15804,15806],{"class":139,"line":188},[137,15799,10287],{"class":143},[137,15801,15476],{"class":157},[137,15803,10954],{"class":143},[137,15805,15481],{"class":284},[137,15807,3276],{"class":157},[137,15809,15810],{"class":139,"line":269},[137,15811,516],{"emptyLinePlaceholder":515},[137,15813,15814,15816,15819,15821,15824],{"class":139,"line":278},[137,15815,13382],{"class":157},[137,15817,15818],{"class":147},"Controller",[137,15820,356],{"class":157},[137,15822,15823],{"class":284},"\"user\"",[137,15825,3155],{"class":157},[137,15827,15828,15830,15832,15835],{"class":139,"line":291},[137,15829,13456],{"class":143},[137,15831,7832],{"class":143},[137,15833,15834],{"class":147}," UserController",[137,15836,256],{"class":157},[137,15838,15839,15841,15843,15845,15847,15850,15852,15854],{"class":139,"line":297},[137,15840,3651],{"class":143},[137,15842,356],{"class":157},[137,15844,15234],{"class":143},[137,15846,13992],{"class":143},[137,15848,15849],{"class":161}," userService",[137,15851,894],{"class":143},[137,15853,15516],{"class":147},[137,15855,15246],{"class":157},[137,15857,15858],{"class":139,"line":302},[137,15859,516],{"emptyLinePlaceholder":515},[137,15861,15862,15864,15867,15869,15872],{"class":139,"line":662},[137,15863,14664],{"class":157},[137,15865,15866],{"class":147},"Post",[137,15868,356],{"class":157},[137,15870,15871],{"class":284},"\"\u002Fsignup\"",[137,15873,3155],{"class":157},[137,15875,15876,15879,15882,15885,15887,15889,15891,15893],{"class":139,"line":667},[137,15877,15878],{"class":147},"    signup",[137,15880,15881],{"class":157},"(@",[137,15883,15884],{"class":147},"Body",[137,15886,3348],{"class":157},[137,15888,15554],{"class":161},[137,15890,894],{"class":143},[137,15892,14657],{"class":147},[137,15894,170],{"class":157},[137,15896,15897,15899,15901,15904,15906],{"class":139,"line":786},[137,15898,5472],{"class":143},[137,15900,365],{"class":364},[137,15902,15903],{"class":157},".userService.",[137,15905,15442],{"class":147},[137,15907,15908],{"class":157},"(userRequest);\n",[137,15910,15911],{"class":139,"line":798},[137,15912,294],{"class":157},[137,15914,15915],{"class":139,"line":803},[137,15916,510],{"class":157},[27,15918,15919],{},"And that's it. We will next create our first user.",[104,15921,15923],{"id":15922},"sign-up-users-with-different-permissions","Sign up Users with different permissions",[27,15925,15926,15927,15932,15933,15938,15939,15942],{},"To test the creation of new users, we will need a tool that will help us with the API server requests. The most popular tool for this purpose is an application called ",[45,15928,15931],{"href":15929,"target":2716,"rel":15930},"https:\u002F\u002Fwww.postman.com\u002F",[2718,2719],"Postman",", it is free and easy to use. If you are a VSCode user, you can use an extension called ",[45,15934,15937],{"href":15935,"target":2716,"rel":15936},"https:\u002F\u002Fmarketplace.visualstudio.com\u002Fitems?itemName=humao.rest-client",[2718,2719],"REST Client",". REST Client allows you to send HTTP request and view the response in Visual Studio Code directly. All endpoints that we are going to test here in this article will be included in the ",[22,15940,15941],{},"api.request.http"," file in the root of the project so you can test this with the REST Client extension.",[27,15944,15945],{},"To signup a new user, we will create the following request in our local server:",[128,15947,15951],{"className":15948,"code":15949,"language":15950,"meta":133,"style":133},"language-http shiki shiki-themes github-light github-dark","POST http:\u002F\u002Flocalhost:3000\u002Fuser\u002Fsignup HTTP\u002F1.1\nContent-Type: application\u002Fjson\n\n{\n    \"email\": \"admin@test.com\",\n    \"password\": \"P@ssword1\",\n    \"firstName\": \"Darrell\",\n    \"lastName\": \"Werner\",\n    \"permissions\": [\"ADMIN\", \"USER\"]\n}\n","http",[22,15952,15953,15958,15963,15967,15972,15977,15982,15987,15992,15997],{"__ignoreMap":133},[137,15954,15955],{"class":139,"line":140},[137,15956,15957],{},"POST http:\u002F\u002Flocalhost:3000\u002Fuser\u002Fsignup HTTP\u002F1.1\n",[137,15959,15960],{"class":139,"line":173},[137,15961,15962],{},"Content-Type: application\u002Fjson\n",[137,15964,15965],{"class":139,"line":188},[137,15966,516],{"emptyLinePlaceholder":515},[137,15968,15969],{"class":139,"line":269},[137,15970,15971],{},"{\n",[137,15973,15974],{"class":139,"line":278},[137,15975,15976],{},"    \"email\": \"admin@test.com\",\n",[137,15978,15979],{"class":139,"line":291},[137,15980,15981],{},"    \"password\": \"P@ssword1\",\n",[137,15983,15984],{"class":139,"line":297},[137,15985,15986],{},"    \"firstName\": \"Darrell\",\n",[137,15988,15989],{"class":139,"line":302},[137,15990,15991],{},"    \"lastName\": \"Werner\",\n",[137,15993,15994],{"class":139,"line":662},[137,15995,15996],{},"    \"permissions\": [\"ADMIN\", \"USER\"]\n",[137,15998,15999],{"class":139,"line":667},[137,16000,510],{},[27,16002,16003,16004,114,16006,16008],{},"With the above request, we have created a User with two permissions: ",[22,16005,14978],{},[22,16007,14981],{},". We can combine all three permissions.",[27,16010,16011,16012,114,16014,16016],{},"Let's now proceed to create two more users, but with different single permissions: ",[22,16013,14981],{},[22,16015,14984],{},". If we try to create a user with permissions that we have't defined in our DTO enum Permissions, we will get an error:",[128,16018,16020],{"className":15948,"code":16019,"language":15950,"meta":133,"style":133},"HTTP\u002F1.1 400 Bad Request\n\n{\n  \"statusCode\": 400,\n  \"message\": [\n    \"each value in permissions must be a valid enum value\"\n  ],\n  \"error\": \"Bad Request\"\n}\n",[22,16021,16022,16027,16031,16035,16040,16045,16050,16055,16060],{"__ignoreMap":133},[137,16023,16024],{"class":139,"line":140},[137,16025,16026],{},"HTTP\u002F1.1 400 Bad Request\n",[137,16028,16029],{"class":139,"line":173},[137,16030,516],{"emptyLinePlaceholder":515},[137,16032,16033],{"class":139,"line":188},[137,16034,15971],{},[137,16036,16037],{"class":139,"line":269},[137,16038,16039],{},"  \"statusCode\": 400,\n",[137,16041,16042],{"class":139,"line":278},[137,16043,16044],{},"  \"message\": [\n",[137,16046,16047],{"class":139,"line":291},[137,16048,16049],{},"    \"each value in permissions must be a valid enum value\"\n",[137,16051,16052],{"class":139,"line":297},[137,16053,16054],{},"  ],\n",[137,16056,16057],{"class":139,"line":302},[137,16058,16059],{},"  \"error\": \"Bad Request\"\n",[137,16061,16062],{"class":139,"line":662},[137,16063,510],{},[104,16065,16067],{"id":16066},"create-guarded-endpoints","Create guarded endpoints",[27,16069,16070,16071,16074,16075,16077,16078,16080],{},"Finally, we will create endpoints by using the Guard that was created earlier. Declare the ",[22,16072,16073],{},"AuthConfig"," as a provider in ",[22,16076,13104],{},". Our ",[22,16079,13104],{}," will appear as follows:",[128,16082,16084],{"className":13299,"code":16083,"language":13301,"meta":133,"style":133},"import { Module } from \"@nestjs\u002Fcommon\";\nimport { AppController } from \".\u002Fapp.controller\";\nimport { UserModule } from \".\u002Fuser\u002Fuser.module\";\nimport { ConfigModule } from \"@nestjs\u002Fconfig\";\nimport { AuthConfig } from \"config\u002Fauth.config\";\n\n@Module({\n    imports: [\n        ConfigModule.forRoot({\n            isGlobal: true,\n            envFilePath: \".env\",\n        }),\n        UserModule,\n    ],\n    controllers: [AppController],\n    providers: [AuthConfig],\n})\nexport class AppModule {}\n",[22,16085,16086,16098,16110,16122,16134,16147,16151,16159,16163,16171,16179,16187,16191,16195,16199,16203,16208,16212],{"__ignoreMap":133},[137,16087,16088,16090,16092,16094,16096],{"class":139,"line":140},[137,16089,10287],{"class":143},[137,16091,13310],{"class":157},[137,16093,10954],{"class":143},[137,16095,13315],{"class":284},[137,16097,3276],{"class":157},[137,16099,16100,16102,16104,16106,16108],{"class":139,"line":173},[137,16101,10287],{"class":143},[137,16103,13324],{"class":157},[137,16105,10954],{"class":143},[137,16107,13329],{"class":284},[137,16109,3276],{"class":157},[137,16111,16112,16114,16116,16118,16120],{"class":139,"line":188},[137,16113,10287],{"class":143},[137,16115,13352],{"class":157},[137,16117,10954],{"class":143},[137,16119,13357],{"class":284},[137,16121,3276],{"class":157},[137,16123,16124,16126,16128,16130,16132],{"class":139,"line":269},[137,16125,10287],{"class":143},[137,16127,13366],{"class":157},[137,16129,10954],{"class":143},[137,16131,13371],{"class":284},[137,16133,3276],{"class":157},[137,16135,16136,16138,16140,16142,16145],{"class":139,"line":278},[137,16137,10287],{"class":143},[137,16139,13898],{"class":157},[137,16141,10954],{"class":143},[137,16143,16144],{"class":284}," \"config\u002Fauth.config\"",[137,16146,3276],{"class":157},[137,16148,16149],{"class":139,"line":291},[137,16150,516],{"emptyLinePlaceholder":515},[137,16152,16153,16155,16157],{"class":139,"line":297},[137,16154,13382],{"class":157},[137,16156,13385],{"class":147},[137,16158,3175],{"class":157},[137,16160,16161],{"class":139,"line":302},[137,16162,13392],{"class":157},[137,16164,16165,16167,16169],{"class":139,"line":662},[137,16166,13397],{"class":157},[137,16168,13400],{"class":147},[137,16170,3175],{"class":157},[137,16172,16173,16175,16177],{"class":139,"line":667},[137,16174,13407],{"class":157},[137,16176,3097],{"class":364},[137,16178,1961],{"class":157},[137,16180,16181,16183,16185],{"class":139,"line":786},[137,16182,13416],{"class":157},[137,16184,13419],{"class":284},[137,16186,1961],{"class":157},[137,16188,16189],{"class":139,"line":798},[137,16190,13426],{"class":157},[137,16192,16193],{"class":139,"line":803},[137,16194,13431],{"class":157},[137,16196,16197],{"class":139,"line":931},[137,16198,13436],{"class":157},[137,16200,16201],{"class":139,"line":1568},[137,16202,13441],{"class":157},[137,16204,16205],{"class":139,"line":1573},[137,16206,16207],{"class":157},"    providers: [AuthConfig],\n",[137,16209,16210],{"class":139,"line":1578},[137,16211,13451],{"class":157},[137,16213,16214,16216,16218,16220],{"class":139,"line":1588},[137,16215,13456],{"class":143},[137,16217,7832],{"class":143},[137,16219,13461],{"class":147},[137,16221,13464],{"class":157},[27,16223,16224],{},"Next, create three new endpoints:",[2569,16226,16227,16238,16247],{},[1006,16228,16229,16232,16233,114,16235,16237],{},[22,16230,16231],{},"\u002Fmorning"," which will be accessed by users with ",[22,16234,14978],{},[22,16236,14981],{}," permissions only,",[1006,16239,16240,16243,16244,16246],{},[22,16241,16242],{},"\u002Fafternoon"," accessed by ",[22,16245,14984],{}," users, and",[1006,16248,16249,16252],{},[22,16250,16251],{},"\u002Fevening"," that will be accessible by everyone.",[27,16254,16255,16256,14571],{},"Our ",[22,16257,13099],{},[128,16259,16261],{"className":13299,"code":16260,"language":13301,"meta":133,"style":133},"import { Controller, Get } from \"@nestjs\u002Fcommon\";\nimport { Auth } from \".\u002Fdecorators\u002Fauth.decorator\";\n\n@Controller()\nexport class AppController {\n    @Get(\"\u002Fmorning\")\n    @Auth(\"ADMIN\", \"USER\")\n    goodMorning() {\n        return \"Good Morning!\";\n    }\n\n    @Get(\"\u002Fafternoon\")\n    @Auth(\"DEVELOPER\")\n    goodAfternoon() {\n        return \"Good Afternoon!\";\n    }\n\n    @Get(\"\u002Fevening\")\n    goodEvening() {\n        return \"Good Evening!\";\n    }\n}\n",[22,16262,16263,16276,16290,16294,16302,16313,16327,16346,16353,16362,16366,16370,16383,16396,16403,16412,16416,16420,16433,16440,16449,16453],{"__ignoreMap":133},[137,16264,16265,16267,16270,16272,16274],{"class":139,"line":140},[137,16266,10287],{"class":143},[137,16268,16269],{"class":157}," { Controller, Get } ",[137,16271,10954],{"class":143},[137,16273,13315],{"class":284},[137,16275,3276],{"class":157},[137,16277,16278,16280,16283,16285,16288],{"class":139,"line":173},[137,16279,10287],{"class":143},[137,16281,16282],{"class":157}," { Auth } ",[137,16284,10954],{"class":143},[137,16286,16287],{"class":284}," \".\u002Fdecorators\u002Fauth.decorator\"",[137,16289,3276],{"class":157},[137,16291,16292],{"class":139,"line":188},[137,16293,516],{"emptyLinePlaceholder":515},[137,16295,16296,16298,16300],{"class":139,"line":269},[137,16297,13382],{"class":157},[137,16299,15818],{"class":147},[137,16301,2754],{"class":157},[137,16303,16304,16306,16308,16311],{"class":139,"line":278},[137,16305,13456],{"class":143},[137,16307,7832],{"class":143},[137,16309,16310],{"class":147}," AppController",[137,16312,256],{"class":157},[137,16314,16315,16317,16320,16322,16325],{"class":139,"line":291},[137,16316,14664],{"class":157},[137,16318,16319],{"class":147},"Get",[137,16321,356],{"class":157},[137,16323,16324],{"class":284},"\"\u002Fmorning\"",[137,16326,3155],{"class":157},[137,16328,16329,16331,16334,16336,16339,16341,16344],{"class":139,"line":297},[137,16330,14664],{"class":157},[137,16332,16333],{"class":147},"Auth",[137,16335,356],{"class":157},[137,16337,16338],{"class":284},"\"ADMIN\"",[137,16340,164],{"class":157},[137,16342,16343],{"class":284},"\"USER\"",[137,16345,3155],{"class":157},[137,16347,16348,16351],{"class":139,"line":302},[137,16349,16350],{"class":147},"    goodMorning",[137,16352,275],{"class":157},[137,16354,16355,16357,16360],{"class":139,"line":662},[137,16356,5472],{"class":143},[137,16358,16359],{"class":284}," \"Good Morning!\"",[137,16361,3276],{"class":157},[137,16363,16364],{"class":139,"line":667},[137,16365,294],{"class":157},[137,16367,16368],{"class":139,"line":786},[137,16369,516],{"emptyLinePlaceholder":515},[137,16371,16372,16374,16376,16378,16381],{"class":139,"line":798},[137,16373,14664],{"class":157},[137,16375,16319],{"class":147},[137,16377,356],{"class":157},[137,16379,16380],{"class":284},"\"\u002Fafternoon\"",[137,16382,3155],{"class":157},[137,16384,16385,16387,16389,16391,16394],{"class":139,"line":803},[137,16386,14664],{"class":157},[137,16388,16333],{"class":147},[137,16390,356],{"class":157},[137,16392,16393],{"class":284},"\"DEVELOPER\"",[137,16395,3155],{"class":157},[137,16397,16398,16401],{"class":139,"line":931},[137,16399,16400],{"class":147},"    goodAfternoon",[137,16402,275],{"class":157},[137,16404,16405,16407,16410],{"class":139,"line":1568},[137,16406,5472],{"class":143},[137,16408,16409],{"class":284}," \"Good Afternoon!\"",[137,16411,3276],{"class":157},[137,16413,16414],{"class":139,"line":1573},[137,16415,294],{"class":157},[137,16417,16418],{"class":139,"line":1578},[137,16419,516],{"emptyLinePlaceholder":515},[137,16421,16422,16424,16426,16428,16431],{"class":139,"line":1588},[137,16423,14664],{"class":157},[137,16425,16319],{"class":147},[137,16427,356],{"class":157},[137,16429,16430],{"class":284},"\"\u002Fevening\"",[137,16432,3155],{"class":157},[137,16434,16435,16438],{"class":139,"line":1601},[137,16436,16437],{"class":147},"    goodEvening",[137,16439,275],{"class":157},[137,16441,16442,16444,16447],{"class":139,"line":3802},[137,16443,5472],{"class":143},[137,16445,16446],{"class":284}," \"Good Evening!\"",[137,16448,3276],{"class":157},[137,16450,16451],{"class":139,"line":3808},[137,16452,294],{"class":157},[137,16454,16455],{"class":139,"line":3822},[137,16456,510],{"class":157},[27,16458,16459],{},"To test our newly created endpoints, we need to have an access token and have that included in the Authorisation header with each request we make to the guarded endpoints.",[27,16461,16462,16463,1017],{},"In order to get the access token we will need to setup the Okta client SDK. We will not go into the details of how to setup the client SDK at the moment. This will be a topic for a separate blog article. Okta has great examples of that topic with all major frontend frameworks. You can find the examples in the following ",[45,16464,2726],{"href":16465,"target":2716,"rel":16466},"https:\u002F\u002Fdeveloper.okta.com\u002F",[2718,2719],[104,16468,16470],{"id":16469},"testing-guarded-endpoints","Testing guarded endpoints",[27,16472,16473],{},"Let's test out the protected endpoints with the access token obtained from the client app. We need to include the following in each request we make in the Authorisation header:",[128,16475,16477],{"className":15948,"code":16476,"language":15950,"meta":133,"style":133},"GET http:\u002F\u002Flocalhost:3000\u002Fmorning HTTP\u002F1.1\nContent-Type: application\u002Fjson\nAuthorization: Bearer {USER_ACCESS_TOKEN}\n\n{}\n",[22,16478,16479,16484,16488,16493,16497],{"__ignoreMap":133},[137,16480,16481],{"class":139,"line":140},[137,16482,16483],{},"GET http:\u002F\u002Flocalhost:3000\u002Fmorning HTTP\u002F1.1\n",[137,16485,16486],{"class":139,"line":173},[137,16487,15962],{},[137,16489,16490],{"class":139,"line":188},[137,16491,16492],{},"Authorization: Bearer {USER_ACCESS_TOKEN}\n",[137,16494,16495],{"class":139,"line":269},[137,16496,516],{"emptyLinePlaceholder":515},[137,16498,16499],{"class":139,"line":278},[137,16500,16501],{},"{}\n",[27,16503,16504,16505,16509],{},"If the access token is from the ",[45,16506,16508],{"href":16507},"mailto:admin@test.com","admin@test.com"," user created earlier, we will get a success 200 response:",[128,16511,16513],{"className":15948,"code":16512,"language":15950,"meta":133,"style":133},"HTTP\u002F1.1 200 OK\nGood Morning!\n",[22,16514,16515,16520],{"__ignoreMap":133},[137,16516,16517],{"class":139,"line":140},[137,16518,16519],{},"HTTP\u002F1.1 200 OK\n",[137,16521,16522],{"class":139,"line":173},[137,16523,16524],{},"Good Morning!\n",[27,16526,16527],{},"But if we try to access via the other two users we earlier created, we will get a 401 Unauthorised response:",[128,16529,16531],{"className":15948,"code":16530,"language":15950,"meta":133,"style":133},"HTTP\u002F1.1 401 Unauthorized\n{\n  \"statusCode\": 401,\n  \"message\": \"Unauthorized\"\n}\n",[22,16532,16533,16538,16542,16547,16552],{"__ignoreMap":133},[137,16534,16535],{"class":139,"line":140},[137,16536,16537],{},"HTTP\u002F1.1 401 Unauthorized\n",[137,16539,16540],{"class":139,"line":173},[137,16541,15971],{},[137,16543,16544],{"class":139,"line":188},[137,16545,16546],{},"  \"statusCode\": 401,\n",[137,16548,16549],{"class":139,"line":269},[137,16550,16551],{},"  \"message\": \"Unauthorized\"\n",[137,16553,16554],{"class":139,"line":278},[137,16555,510],{},[27,16557,16558],{},"And that's all!",[27,16560,10126,16561,1017],{},[45,16562,2726],{"href":16563,"target":2716,"rel":16564},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fnest-auth-with-okta",[2718,2719],[104,16566,2567],{"id":2566},[2569,16568,16569,16572,16575,16592,16599],{},[1006,16570,16571],{},"This blog article shows how to sign up and sign in users with specific roles. Then giving those users access to only specific endpoints based on their roles. To accomplish that, we used Okta to help us with the users Authentication and Authorisation.",[1006,16573,16574],{},"Okta's Developer Edition provides a free with no time limit access to a variety of key developer features.",[1006,16576,16577,16578],{},"To create and validate users in our app we used two packages provided by Okta:\n",[1003,16579,16580,16586],{},[1006,16581,16582,16583,1017],{},"Okta JWT Verifier for Node.js ",[45,16584,2726],{"href":13815,"target":2716,"rel":16585},[2718,2719],[1006,16587,16588,16589,1017],{},"Okta Node.js Management SDK ",[45,16590,2726],{"href":14462,"target":2716,"rel":16591},[2718,2719],[1006,16593,16594,16595,1017],{},"Nest has built in Guards mechanisms to handle the protected endpoints. The official documentation has great examples on how to use ",[45,16596,16598],{"href":13794,"target":2716,"rel":16597},[2718,2719],"guards",[1006,16600,16601,16602,1017],{},"To be able to get the access token, we will need to setup the Okta client SDK. Okta has great documentation and examples on that topic with all the major frontend frameworks. You can find these examples in the following ",[45,16603,2726],{"href":16465,"target":2716,"rel":16604},[2718,2719],[2617,16606,16607],{},"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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":133,"searchDepth":173,"depth":173,"links":16609},[16610,16611,16612,16613,16614,16615,16616,16617,16618,16623,16624,16625,16626],{"id":12851,"depth":173,"text":12852},{"id":12881,"depth":173,"text":12882},{"id":12911,"depth":173,"text":12912},{"id":13120,"depth":173,"text":13121},{"id":13158,"depth":173,"text":13159},{"id":13254,"depth":173,"text":13255},{"id":13511,"depth":173,"text":13512},{"id":13784,"depth":173,"text":13785},{"id":14455,"depth":173,"text":14456,"children":16619},[16620,16621,16622],{"id":14485,"depth":188,"text":14486},{"id":14520,"depth":188,"text":14521},{"id":15127,"depth":188,"text":15128},{"id":15922,"depth":173,"text":15923},{"id":16066,"depth":173,"text":16067},{"id":16469,"depth":173,"text":16470},{"id":2566,"depth":173,"text":2567},"In this blog article we will be creating a Nest application where users (with different roles) can sign-up and sign-in to the application. Specific permissions can be configured for each user access to specific endpoints, based on the user role. We are going to use Okta to help us with user Authentication and Authorisation. Okta is an Identity as a Service (IDaaS). This is a cloud-based authentication or identity management subscription service. Okta can be used for a number of different applications such as Adaptive multi-factor authentication, single sign-on, Universal Directory etc. Nest is a progressive Node.js framework for building efficient, reliable and scalable server-side applications with TypeScript.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1664708491\u002Fblog\u002Fnestjs-auth-authorisation-with-okta\u002Fnestjs-auth-authorisation-with-okta",[12814,12816,12817,3537,16630,16631,5299,5300],"Authorization","Okta Auth",{},"\u002F2022\u002F10\u002F03\u002Fnestjs-auth-authorisation-with-okta","3rd Oct 2022",{"title":12814,"description":16627},"2022\u002F10\u002F03\u002Fnestjs-auth-authorisation-with-okta","qCh6hQGx1dev9xil6OGdrGtTbGE0z7Rne77_HmcmhCs",{"id":16639,"title":16640,"articleTags":16641,"author":11,"blog":12,"body":16642,"description":19574,"extension":2649,"image":19575,"keywords":19576,"meta":19578,"navigation":515,"path":19579,"published":19580,"readTime":1568,"seo":19581,"stem":19582,"type":2662,"__hash__":19583},"content\u002F2022\u002F10\u002F07\u002Fnestjs-authorisation-with-firebase-auth.md","Nest.js Authorisation with Firebase Auth",[12816,2668,2669],{"type":14,"value":16643,"toc":19555},[16644,16647,16661,16663,16667,16672,16674,16679,16687,16689,16693,16705,16708,16710,16716,16730,16732,16746,16750,16779,16805,16810,16833,16847,16859,16863,16867,16871,16875,16879,16881,16885,16893,16896,16902,16905,16910,16913,16915,16917,16919,16933,16941,16943,16959,16965,16967,16983,16985,16987,16993,17007,17017,17155,17177,17181,17186,17190,17193,17199,17204,17210,17213,17230,17233,17245,17254,17270,17276,17504,17506,17508,17511,17517,17527,17859,17867,17946,17949,17952,17956,17959,17963,17966,17972,17975,17980,17983,17985,17991,18007,18019,18379,18388,18396,18504,18506,18514,18603,18612,18889,18894,19032,19035,19037,19047,19049,19055,19061,19069,19074,19076,19085,19239,19241,19259,19263,19440,19442,19449,19451,19454,19478,19482,19494,19496,19520,19523,19529,19531,19553],[17,16645,16640],{"id":16646},"nestjs-authorisation-with-firebase-auth",[27,16648,16649],{},[30,16650,16651,36,16653,40,16655],{},[33,16652],{"value":35},[33,16654],{"value":39},[42,16656,16657],{},[45,16658,16659],{"href":47},[33,16660],{"value":50},[52,16662],{":tags":54},[56,16664],{":audio-src":16665,":transcript-src":16666},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F10\u002F07\u002Fnestjs-authorisation-with-firebase-auth\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F10\u002F07\u002Fnestjs-authorisation-with-firebase-auth\u002Fsummary.json",[27,16668,16669],{},[63,16670],{"alt":12847,"src":16671},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1664868269\u002Fblog\u002Fnestjs-authorisation-with-firebase-auth\u002Fnestjs-authorisation-with-firebase-auth",[104,16673,12852],{"id":12851},[27,16675,16676,12860],{},[45,16677,12859],{"href":12857,"target":2716,"rel":16678},[2718,2719],[27,16680,12863,16681,12869,16684,12875],{},[45,16682,12868],{"href":12866,"target":2716,"rel":16683},[2718,2719],[45,16685,12874],{"href":12872,"target":2716,"rel":16686},[2718,2719],[27,16688,12878],{},[104,16690,16692],{"id":16691},"what-is-firebase","What is Firebase?",[27,16694,16695,16699,16700,16704],{},[45,16696,2668],{"href":16697,"target":2716,"rel":16698},"https:\u002F\u002Ffirebase.google.com\u002F",[2718,2719]," as a platform that offers a wide range of services to developers to build, improve, and grow their apps with little or almost no effort. This includes services like authentication, databases, analytics, file storage, push messaging and more. When it comes to user authentication, Firebase provides an ",[45,16701,3537],{"href":16702,"target":2716,"rel":16703},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fauth",[2718,2719]," service that allows for codes to be written in order for users to be logged into an app from the client side. Firebase also provides an Admin SDK that allows developers to integrate their applications with custom backend, if required.",[27,16706,16707],{},"In this blog article we will be creating a Nest application where users (with different roles) can sign-up and sign-in to the application. Specific permissions can be configured for each user access to specific endpoints, based on the user role. We are going to use Firebase Auth to help us with user Authentication and Authorisation.",[104,16709,12912],{"id":12911},[27,16711,16712,16713,12921],{},"Before we continue let’s first install ",[45,16714,12920],{"href":12918,"target":2716,"rel":16715},[2718,2719],[128,16717,16718],{"className":8665,"code":12924,"language":8667,"meta":133,"style":133},[22,16719,16720],{"__ignoreMap":133},[137,16721,16722,16724,16726,16728],{"class":139,"line":140},[137,16723,9536],{"class":147},[137,16725,10268],{"class":284},[137,16727,12935],{"class":364},[137,16729,12938],{"class":284},[27,16731,12941],{},[128,16733,16735],{"className":8665,"code":16734,"language":8667,"meta":133,"style":133},"nest new nest-auth-with-firebase-auth\n",[22,16736,16737],{"__ignoreMap":133},[137,16738,16739,16741,16743],{"class":139,"line":140},[137,16740,12951],{"class":147},[137,16742,1426],{"class":284},[137,16744,16745],{"class":284}," nest-auth-with-firebase-auth\n",[27,16747,12959,16748,12962],{},[22,16749,9536],{},[128,16751,16753],{"className":8665,"code":16752,"language":8667,"meta":133,"style":133},"? Which package manager would you ❤️ to use?\n  npm\n❯ yarn\n  pnpm\n",[22,16754,16755,16763,16768,16775],{"__ignoreMap":133},[137,16756,16757,16759,16761],{"class":139,"line":140},[137,16758,12972],{"class":143},[137,16760,12975],{"class":157},[137,16762,12978],{"class":143},[137,16764,16765],{"class":139,"line":173},[137,16766,16767],{"class":147},"  npm\n",[137,16769,16770,16772],{"class":139,"line":188},[137,16771,12983],{"class":147},[137,16773,16774],{"class":284}," yarn\n",[137,16776,16777],{"class":139,"line":269},[137,16778,12996],{"class":147},[128,16780,16781],{"className":8665,"code":12999,"language":8667,"meta":133,"style":133},[22,16782,16783,16793],{"__ignoreMap":133},[137,16784,16785,16787,16789,16791],{"class":139,"line":140},[137,16786,12972],{"class":143},[137,16788,12975],{"class":157},[137,16790,12972],{"class":143},[137,16792,12986],{"class":157},[137,16794,16795,16797,16799,16801,16803],{"class":139,"line":173},[137,16796,13016],{"class":147},[137,16798,13019],{"class":284},[137,16800,13022],{"class":284},[137,16802,13025],{"class":284},[137,16804,13028],{"class":284},[27,16806,13031,16807,13035],{},[22,16808,16809],{},"nest-auth-with-firebase-auth",[128,16811,16813],{"className":8665,"code":16812,"language":8667,"meta":133,"style":133},"$ cd nest-auth-with-firebase-auth\n$ npm run start\n",[22,16814,16815,16823],{"__ignoreMap":133},[137,16816,16817,16819,16821],{"class":139,"line":140},[137,16818,13045],{"class":147},[137,16820,13048],{"class":284},[137,16822,16745],{"class":284},[137,16824,16825,16827,16829,16831],{"class":139,"line":173},[137,16826,13045],{"class":147},[137,16828,13057],{"class":284},[137,16830,9578],{"class":284},[137,16832,13062],{"class":284},[27,16834,13065,16835,16840,13072,16845],{},[45,16836,16838],{"href":13068,"target":2716,"rel":16837},[2718,2719],[42,16839],{},[42,16841,16842],{},[45,16843,13068],{"href":13068,"rel":16844},[10924],[42,16846,13075],{},[27,16848,16849,16850,16852,16853,16855,16856,16858],{},"Now, let’s have a look at the folder structure in our Nest project. Inside the ",[22,16851,16809],{}," directory we have ",[22,16854,13084],{},", a few other boilerplate files and a ",[22,16857,13088],{}," directory populated with several core files.",[27,16860,16861],{},[22,16862,13094],{},[27,16864,16865],{},[22,16866,13099],{},[27,16868,16869],{},[22,16870,13104],{},[27,16872,16873],{},[22,16874,13109],{},[27,16876,16877],{},[22,16878,13114],{},[27,16880,13117],{},[104,16882,16884],{"id":16883},"getting-started-with-firebase-auth","Getting started with Firebase Auth",[27,16886,16887,16888,1017],{},"Before we start working with Firebase, create a Firebase project in the ",[45,16889,16891],{"href":2715,"target":2716,"rel":16890},[2718,2719],[42,16892,2720],{},[27,16894,16895],{},"We can do so by clicking Add Project from the Firebase Console:",[27,16897,16898],{},[63,16899],{"alt":16900,"src":16901},"Creating account in Firebase Console","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1664958711\u002Fblog\u002Fnestjs-authorisation-with-firebase-auth\u002Fnestjs-authorisation-with-firebase-auth_dgoo4f",[27,16903,16904],{},"Enter the project name in the prompt and follow the next three steps to setup the project. In a short period of time, the project will be created.",[27,16906,16907],{},[63,16908],{"alt":16900,"src":16909},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1664958815\u002Fblog\u002Fnestjs-authorisation-with-firebase-auth\u002Fnestjs-authorisation-with-firebase-auth_fc8xge",[27,16911,16912],{},"That’s all we need for now. Later in the blog article, we will come back to setup some more things. Until then, let’s move to the next section where we start implementing our Authentication in Nest.",[104,16914,13159],{"id":13158},[27,16916,13162],{},[27,16918,13165],{},[128,16920,16921],{"className":8665,"code":13168,"language":8667,"meta":133,"style":133},[22,16922,16923],{"__ignoreMap":133},[137,16924,16925,16927,16929,16931],{"class":139,"line":140},[137,16926,12951],{"class":147},[137,16928,13177],{"class":284},[137,16930,13180],{"class":284},[137,16932,13183],{"class":284},[27,16934,13186,16935,13190,16937,13194,16939,13197],{},[22,16936,13189],{},[22,16938,13193],{},[22,16940,13104],{},[27,16942,13200],{},[128,16944,16945],{"className":8665,"code":13203,"language":8667,"meta":133,"style":133},[22,16946,16947],{"__ignoreMap":133},[137,16948,16949,16951,16953,16955,16957],{"class":139,"line":140},[137,16950,12951],{"class":147},[137,16952,13177],{"class":284},[137,16954,13214],{"class":284},[137,16956,13217],{"class":284},[137,16958,13220],{"class":364},[27,16960,13223,16961,13227,16963,10277],{},[22,16962,13226],{},[22,16964,13230],{},[27,16966,13233],{},[128,16968,16969],{"className":8665,"code":13236,"language":8667,"meta":133,"style":133},[22,16970,16971],{"__ignoreMap":133},[137,16972,16973,16975,16977,16979,16981],{"class":139,"line":140},[137,16974,12951],{"class":147},[137,16976,13177],{"class":284},[137,16978,13247],{"class":284},[137,16980,13217],{"class":284},[137,16982,13220],{"class":364},[104,16984,13255],{"id":13254},[27,16986,13258],{},[27,16988,16989,16990,16992],{},"Nest provides a ",[22,16991,13264],{}," package out-of-the box. In order to setup our configuration, we need to install it first.",[128,16994,16995],{"className":8665,"code":13268,"language":8667,"meta":133,"style":133},[22,16996,16997],{"__ignoreMap":133},[137,16998,16999,17001,17003,17005],{"class":139,"line":140},[137,17000,9536],{"class":147},[137,17002,13277],{"class":284},[137,17004,13280],{"class":364},[137,17006,13283],{"class":284},[27,17008,17009,17010,17012,17013,13293,17015,13296],{},"Once the installation process is complete, we import the ",[22,17011,13289],{},". We need to import the module into the root ",[22,17014,13104],{},[22,17016,13104],{},[128,17018,17019],{"className":13299,"code":13300,"language":13301,"meta":133,"style":133},[22,17020,17021,17033,17045,17057,17069,17081,17085,17093,17097,17105,17113,17121,17125,17129,17133,17137,17141,17145],{"__ignoreMap":133},[137,17022,17023,17025,17027,17029,17031],{"class":139,"line":140},[137,17024,10287],{"class":143},[137,17026,13310],{"class":157},[137,17028,10954],{"class":143},[137,17030,13315],{"class":284},[137,17032,3276],{"class":157},[137,17034,17035,17037,17039,17041,17043],{"class":139,"line":173},[137,17036,10287],{"class":143},[137,17038,13324],{"class":157},[137,17040,10954],{"class":143},[137,17042,13329],{"class":284},[137,17044,3276],{"class":157},[137,17046,17047,17049,17051,17053,17055],{"class":139,"line":188},[137,17048,10287],{"class":143},[137,17050,13338],{"class":157},[137,17052,10954],{"class":143},[137,17054,13343],{"class":284},[137,17056,3276],{"class":157},[137,17058,17059,17061,17063,17065,17067],{"class":139,"line":269},[137,17060,10287],{"class":143},[137,17062,13352],{"class":157},[137,17064,10954],{"class":143},[137,17066,13357],{"class":284},[137,17068,3276],{"class":157},[137,17070,17071,17073,17075,17077,17079],{"class":139,"line":278},[137,17072,10287],{"class":143},[137,17074,13366],{"class":157},[137,17076,10954],{"class":143},[137,17078,13371],{"class":284},[137,17080,3276],{"class":157},[137,17082,17083],{"class":139,"line":291},[137,17084,516],{"emptyLinePlaceholder":515},[137,17086,17087,17089,17091],{"class":139,"line":297},[137,17088,13382],{"class":157},[137,17090,13385],{"class":147},[137,17092,3175],{"class":157},[137,17094,17095],{"class":139,"line":302},[137,17096,13392],{"class":157},[137,17098,17099,17101,17103],{"class":139,"line":662},[137,17100,13397],{"class":157},[137,17102,13400],{"class":147},[137,17104,3175],{"class":157},[137,17106,17107,17109,17111],{"class":139,"line":667},[137,17108,13407],{"class":157},[137,17110,3097],{"class":364},[137,17112,1961],{"class":157},[137,17114,17115,17117,17119],{"class":139,"line":786},[137,17116,13416],{"class":157},[137,17118,13419],{"class":284},[137,17120,1961],{"class":157},[137,17122,17123],{"class":139,"line":798},[137,17124,13426],{"class":157},[137,17126,17127],{"class":139,"line":803},[137,17128,13431],{"class":157},[137,17130,17131],{"class":139,"line":931},[137,17132,13436],{"class":157},[137,17134,17135],{"class":139,"line":1568},[137,17136,13441],{"class":157},[137,17138,17139],{"class":139,"line":1573},[137,17140,13446],{"class":157},[137,17142,17143],{"class":139,"line":1578},[137,17144,13451],{"class":157},[137,17146,17147,17149,17151,17153],{"class":139,"line":1588},[137,17148,13456],{"class":143},[137,17150,7832],{"class":143},[137,17152,13461],{"class":147},[137,17154,13464],{"class":157},[27,17156,4370,17157,13470,17159,13473,17161,13477,17163,13480,17165,17167,17168,13477,17170,17172,17173,13493,17175,1017],{},[22,17158,13469],{},[22,17160,13289],{},[22,17162,13476],{},[22,17164,3097],{},[22,17166,13289],{}," in other modules so we don’t need to import our module into each module we use. Secondly, set ",[22,17169,13486],{},[22,17171,13489],{},". This will load and parse a ",[22,17174,13489],{},[22,17176,13496],{},[27,17178,13499,17179,13502],{},[22,17180,13489],{},[27,17182,13505,17183,17185],{},[22,17184,13289],{}," . Let’s move on to setting up Firebase Auth in our Nest app.",[104,17187,17189],{"id":17188},"firebase-setup","Firebase Setup",[27,17191,17192],{},"We will need to go back to our Firebase project console to generate a private key which allows us configure the Firebase Node.js Admin SDK. To generate the private key, navigate to Project Overview → Project Settings → Service Accounts → Generate new private key.",[27,17194,17195],{},[63,17196],{"alt":17197,"src":17198},"Generate new private key in Firebase Console","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1664959187\u002Fblog\u002Fnestjs-authorisation-with-firebase-auth\u002Fnestjs-authorisation-with-firebase-auth_gzv6ae",[27,17200,17201],{},[63,17202],{"alt":17197,"src":17203},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1664959188\u002Fblog\u002Fnestjs-authorisation-with-firebase-auth\u002Fnestjs-authorisation-with-firebase-auth_y3jfy0",[27,17205,17206,17207,10277],{},"This will download a ",[22,17208,17209],{},"serviceAccountKey.json",[27,17211,17212],{},"Now, we have two choices:",[2569,17214,17215,17224],{},[1006,17216,17217,17218,17220,17221,17223],{},"We can set all values from the ",[22,17219,17209],{}," as environmental variables in our ",[22,17222,13489],{}," file or",[1006,17225,17226,17227,17229],{},"We can import ",[22,17228,17209],{}," directly in our app.",[27,17231,17232],{},"In this example, we are going to use the second option. Feel free to try the first one if that works better to you.",[27,17234,17235,17236,17238,17239,17241,17242,1017],{},"We will next create a ",[22,17237,13567],{}," directory at the root of the Nest project. Inside the directory, copy this file ",[22,17240,17209],{}," and rename it to: ",[22,17243,17244],{},"firebaseServiceAccountKey.json",[27,17246,17247,17248,17253],{},"To proceed with the ",[45,17249,17252],{"href":17250,"target":2716,"rel":17251},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Ffirebase-admin",[2718,2719],"Firebase Node.js Admin SDK"," first we need to install it:",[128,17255,17257],{"className":8665,"code":17256,"language":8667,"meta":133,"style":133},"yarn add firebase-admin\n",[22,17258,17259],{"__ignoreMap":133},[137,17260,17261,17264,17267],{"class":139,"line":140},[137,17262,17263],{"class":147},"yarn",[137,17265,17266],{"class":284}," add",[137,17268,17269],{"class":284}," firebase-admin\n",[27,17271,17272,17273,13575],{},"Next, create new file called ",[22,17274,17275],{},"firebase.setup.ts",[128,17277,17279],{"className":13299,"code":17278,"language":13301,"meta":133,"style":133},"import { Injectable, OnApplicationBootstrap } from \"@nestjs\u002Fcommon\";\nimport { readFile } from \"fs\u002Fpromises\";\nimport * as admin from \"firebase-admin\";\nlet app: admin.app.App = null;\n@Injectable()\nexport class FirebaseAdmin implements OnApplicationBootstrap {\n    async onApplicationBootstrap() {\n        if (!app) {\n            const firebaseServiceAccountFile = await readFile(\".\u002Fconfig\u002FfirebaseServiceAccountKey.json\", \"utf8\");\n            const serviceAccount = await JSON.parse(firebaseServiceAccountFile);\n            app = admin.initializeApp({\n                credential: admin.credential.cert(serviceAccount),\n            });\n        }\n    }\n    setup() {\n        return app;\n    }\n}\n",[22,17280,17281,17294,17308,17326,17352,17360,17376,17385,17397,17423,17445,17460,17471,17475,17479,17483,17489,17496,17500],{"__ignoreMap":133},[137,17282,17283,17285,17288,17290,17292],{"class":139,"line":140},[137,17284,10287],{"class":143},[137,17286,17287],{"class":157}," { Injectable, OnApplicationBootstrap } ",[137,17289,10954],{"class":143},[137,17291,13315],{"class":284},[137,17293,3276],{"class":157},[137,17295,17296,17298,17301,17303,17306],{"class":139,"line":173},[137,17297,10287],{"class":143},[137,17299,17300],{"class":157}," { readFile } ",[137,17302,10954],{"class":143},[137,17304,17305],{"class":284}," \"fs\u002Fpromises\"",[137,17307,3276],{"class":157},[137,17309,17310,17312,17314,17316,17319,17321,17324],{"class":139,"line":188},[137,17311,10287],{"class":143},[137,17313,13878],{"class":364},[137,17315,13881],{"class":143},[137,17317,17318],{"class":157}," admin ",[137,17320,10954],{"class":143},[137,17322,17323],{"class":284}," \"firebase-admin\"",[137,17325,3276],{"class":157},[137,17327,17328,17330,17332,17334,17337,17339,17341,17343,17346,17348,17350],{"class":139,"line":269},[137,17329,11498],{"class":143},[137,17331,15064],{"class":157},[137,17333,894],{"class":143},[137,17335,17336],{"class":147}," admin",[137,17338,1017],{"class":157},[137,17340,4302],{"class":147},[137,17342,1017],{"class":157},[137,17344,17345],{"class":147},"App",[137,17347,151],{"class":143},[137,17349,3417],{"class":364},[137,17351,3276],{"class":157},[137,17353,17354,17356,17358],{"class":139,"line":278},[137,17355,13382],{"class":157},[137,17357,13604],{"class":147},[137,17359,2754],{"class":157},[137,17361,17362,17364,17366,17369,17371,17374],{"class":139,"line":291},[137,17363,13456],{"class":143},[137,17365,7832],{"class":143},[137,17367,17368],{"class":147}," FirebaseAdmin",[137,17370,13943],{"class":143},[137,17372,17373],{"class":147}," OnApplicationBootstrap",[137,17375,256],{"class":157},[137,17377,17378,17380,17383],{"class":139,"line":297},[137,17379,15546],{"class":143},[137,17381,17382],{"class":147}," onApplicationBootstrap",[137,17384,275],{"class":157},[137,17386,17387,17389,17391,17394],{"class":139,"line":302},[137,17388,5496],{"class":143},[137,17390,158],{"class":157},[137,17392,17393],{"class":143},"!",[137,17395,17396],{"class":157},"app) {\n",[137,17398,17399,17401,17404,17406,17408,17411,17413,17416,17418,17421],{"class":139,"line":662},[137,17400,5772],{"class":143},[137,17402,17403],{"class":364}," firebaseServiceAccountFile",[137,17405,151],{"class":143},[137,17407,15069],{"class":143},[137,17409,17410],{"class":147}," readFile",[137,17412,356],{"class":157},[137,17414,17415],{"class":284},"\".\u002Fconfig\u002FfirebaseServiceAccountKey.json\"",[137,17417,164],{"class":157},[137,17419,17420],{"class":284},"\"utf8\"",[137,17422,1502],{"class":157},[137,17424,17425,17427,17430,17432,17434,17437,17439,17442],{"class":139,"line":667},[137,17426,5772],{"class":143},[137,17428,17429],{"class":364}," serviceAccount",[137,17431,151],{"class":143},[137,17433,15069],{"class":143},[137,17435,17436],{"class":364}," JSON",[137,17438,1017],{"class":157},[137,17440,17441],{"class":147},"parse",[137,17443,17444],{"class":157},"(firebaseServiceAccountFile);\n",[137,17446,17447,17450,17452,17455,17458],{"class":139,"line":786},[137,17448,17449],{"class":157},"            app ",[137,17451,253],{"class":143},[137,17453,17454],{"class":157}," admin.",[137,17456,17457],{"class":147},"initializeApp",[137,17459,3175],{"class":157},[137,17461,17462,17465,17468],{"class":139,"line":798},[137,17463,17464],{"class":157},"                credential: admin.credential.",[137,17466,17467],{"class":147},"cert",[137,17469,17470],{"class":157},"(serviceAccount),\n",[137,17472,17473],{"class":139,"line":803},[137,17474,14336],{"class":157},[137,17476,17477],{"class":139,"line":931},[137,17478,1966],{"class":157},[137,17480,17481],{"class":139,"line":1568},[137,17482,294],{"class":157},[137,17484,17485,17487],{"class":139,"line":1573},[137,17486,15255],{"class":147},[137,17488,275],{"class":157},[137,17490,17491,17493],{"class":139,"line":1578},[137,17492,5472],{"class":143},[137,17494,17495],{"class":157}," app;\n",[137,17497,17498],{"class":139,"line":1588},[137,17499,294],{"class":157},[137,17501,17502],{"class":139,"line":1601},[137,17503,510],{"class":157},[27,17505,13781],{},[104,17507,13785],{"id":13784},[27,17509,17510],{},"The purpose of implementing an authentication and authorisation is to be able to protect certain parts of the application to only authenticated users with a specific permissions. This might sound complicated, but with Nest and Firebase this implementation is made straightforward.",[27,17512,17513,17514,1017],{},"Nest has Guards to handle all of this out of the box. Guards have a single responsibility. They determine whether a given request should be handled by the route handler or not, depending on certain conditions like permissions or roles. For more details on how Guards work inside Nest, please refer to this ",[45,17515,2726],{"href":13794,"target":2716,"rel":17516},[2718,2719],[27,17518,17519,17520,13801,17522,13805,17524,17526],{},"Let’s define our Guard. Create a new directory inside the ",[22,17521,13088],{},[22,17523,13804],{},[22,17525,13808],{}," where we will place the logic for our Firebase authentication Guard.",[128,17528,17530],{"className":13299,"code":17529,"language":13301,"meta":133,"style":133},"import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from \"@nestjs\u002Fcommon\";\nimport { Reflector } from \"@nestjs\u002Fcore\";\nimport { FirebaseAdmin } from \"..\u002F..\u002Fconfig\u002Ffirebase.setup\";\n\n@Injectable()\nexport class AuthGuard implements CanActivate {\n    constructor(\n        private reflector: Reflector,\n        private readonly admin: FirebaseAdmin\n    ) {}\n\n    async canActivate(context: ExecutionContext): Promise\u003Cboolean> {\n        const app = this.admin.setup();\n        const idToken = context.getArgs()[0]?.headers?.authorization.split(\" \")[1];\n\n        const permissions = this.reflector.get\u003Cstring[]>(\"permissions\", context.getHandler());\n        try {\n            const claims = await app.auth().verifyIdToken(idToken);\n\n            if (claims.role === permissions[0]) {\n                return true;\n            }\n            throw new UnauthorizedException();\n        } catch (error) {\n            console.log(\"Error\", error);\n            throw new UnauthorizedException();\n        }\n    }\n}\n",[22,17531,17532,17544,17556,17570,17574,17582,17597,17603,17615,17628,17632,17636,17663,17680,17711,17715,17743,17749,17772,17776,17793,17801,17805,17815,17823,17837,17847,17851,17855],{"__ignoreMap":133},[137,17533,17534,17536,17538,17540,17542],{"class":139,"line":140},[137,17535,10287],{"class":143},[137,17537,13851],{"class":157},[137,17539,10954],{"class":143},[137,17541,13315],{"class":284},[137,17543,3276],{"class":157},[137,17545,17546,17548,17550,17552,17554],{"class":139,"line":173},[137,17547,10287],{"class":143},[137,17549,13912],{"class":157},[137,17551,10954],{"class":143},[137,17553,13917],{"class":284},[137,17555,3276],{"class":157},[137,17557,17558,17560,17563,17565,17568],{"class":139,"line":188},[137,17559,10287],{"class":143},[137,17561,17562],{"class":157}," { FirebaseAdmin } ",[137,17564,10954],{"class":143},[137,17566,17567],{"class":284}," \"..\u002F..\u002Fconfig\u002Ffirebase.setup\"",[137,17569,3276],{"class":157},[137,17571,17572],{"class":139,"line":269},[137,17573,516],{"emptyLinePlaceholder":515},[137,17575,17576,17578,17580],{"class":139,"line":278},[137,17577,13382],{"class":157},[137,17579,13604],{"class":147},[137,17581,2754],{"class":157},[137,17583,17584,17586,17588,17591,17593,17595],{"class":139,"line":291},[137,17585,13456],{"class":143},[137,17587,7832],{"class":143},[137,17589,17590],{"class":147}," AuthGuard",[137,17592,13943],{"class":143},[137,17594,13946],{"class":147},[137,17596,256],{"class":157},[137,17598,17599,17601],{"class":139,"line":297},[137,17600,3651],{"class":143},[137,17602,11813],{"class":157},[137,17604,17605,17607,17609,17611,17613],{"class":139,"line":302},[137,17606,13975],{"class":143},[137,17608,13978],{"class":161},[137,17610,894],{"class":143},[137,17612,13983],{"class":147},[137,17614,1961],{"class":157},[137,17616,17617,17619,17621,17623,17625],{"class":139,"line":662},[137,17618,13975],{"class":143},[137,17620,13992],{"class":143},[137,17622,17336],{"class":161},[137,17624,894],{"class":143},[137,17626,17627],{"class":147}," FirebaseAdmin\n",[137,17629,17630],{"class":139,"line":667},[137,17631,14005],{"class":157},[137,17633,17634],{"class":139,"line":786},[137,17635,516],{"emptyLinePlaceholder":515},[137,17637,17638,17640,17643,17645,17647,17649,17651,17653,17655,17657,17659,17661],{"class":139,"line":798},[137,17639,15546],{"class":143},[137,17641,17642],{"class":147}," canActivate",[137,17644,356],{"class":157},[137,17646,14097],{"class":161},[137,17648,894],{"class":143},[137,17650,14102],{"class":147},[137,17652,14105],{"class":157},[137,17654,894],{"class":143},[137,17656,14116],{"class":147},[137,17658,4033],{"class":157},[137,17660,14121],{"class":364},[137,17662,14136],{"class":157},[137,17664,17665,17667,17669,17671,17673,17676,17678],{"class":139,"line":803},[137,17666,3008],{"class":143},[137,17668,15064],{"class":364},[137,17670,151],{"class":143},[137,17672,365],{"class":364},[137,17674,17675],{"class":157},".admin.",[137,17677,15609],{"class":147},[137,17679,924],{"class":157},[137,17681,17682,17684,17687,17689,17691,17693,17695,17697,17699,17701,17703,17705,17707,17709],{"class":139,"line":931},[137,17683,3008],{"class":143},[137,17685,17686],{"class":364}," idToken",[137,17688,151],{"class":143},[137,17690,14204],{"class":157},[137,17692,14207],{"class":147},[137,17694,14210],{"class":157},[137,17696,6044],{"class":364},[137,17698,14215],{"class":157},[137,17700,8537],{"class":147},[137,17702,356],{"class":157},[137,17704,8542],{"class":284},[137,17706,14224],{"class":157},[137,17708,6065],{"class":364},[137,17710,5727],{"class":157},[137,17712,17713],{"class":139,"line":1568},[137,17714,516],{"emptyLinePlaceholder":515},[137,17716,17717,17719,17721,17723,17725,17727,17729,17731,17733,17735,17737,17739,17741],{"class":139,"line":1573},[137,17718,3008],{"class":143},[137,17720,14143],{"class":364},[137,17722,151],{"class":143},[137,17724,365],{"class":364},[137,17726,14150],{"class":157},[137,17728,14153],{"class":147},[137,17730,4033],{"class":157},[137,17732,14158],{"class":364},[137,17734,14161],{"class":157},[137,17736,14164],{"class":284},[137,17738,14167],{"class":157},[137,17740,14170],{"class":147},[137,17742,14173],{"class":157},[137,17744,17745,17747],{"class":139,"line":1578},[137,17746,15697],{"class":143},[137,17748,256],{"class":157},[137,17750,17751,17753,17756,17758,17760,17762,17764,17767,17769],{"class":139,"line":1588},[137,17752,5772],{"class":143},[137,17754,17755],{"class":364}," claims",[137,17757,151],{"class":143},[137,17759,15069],{"class":143},[137,17761,15103],{"class":157},[137,17763,2751],{"class":147},[137,17765,17766],{"class":157},"().",[137,17768,2983],{"class":147},[137,17770,17771],{"class":157},"(idToken);\n",[137,17773,17774],{"class":139,"line":1601},[137,17775,516],{"emptyLinePlaceholder":515},[137,17777,17778,17780,17783,17785,17788,17790],{"class":139,"line":3802},[137,17779,5747],{"class":143},[137,17781,17782],{"class":157}," (claims.role ",[137,17784,5502],{"class":143},[137,17786,17787],{"class":157}," permissions[",[137,17789,6044],{"class":364},[137,17791,17792],{"class":157},"]) {\n",[137,17794,17795,17797,17799],{"class":139,"line":3808},[137,17796,5761],{"class":143},[137,17798,14286],{"class":364},[137,17800,3276],{"class":157},[137,17802,17803],{"class":139,"line":3822},[137,17804,760],{"class":157},[137,17806,17807,17809,17811,17813],{"class":139,"line":3827},[137,17808,15739],{"class":143},[137,17810,1426],{"class":143},[137,17812,14329],{"class":147},[137,17814,924],{"class":157},[137,17816,17817,17819,17821],{"class":139,"line":3832},[137,17818,15729],{"class":157},[137,17820,2807],{"class":143},[137,17822,15734],{"class":157},[137,17824,17825,17827,17829,17831,17834],{"class":139,"line":3840},[137,17826,1493],{"class":157},[137,17828,353],{"class":147},[137,17830,356],{"class":157},[137,17832,17833],{"class":284},"\"Error\"",[137,17835,17836],{"class":157},", error);\n",[137,17838,17839,17841,17843,17845],{"class":139,"line":3846},[137,17840,15739],{"class":143},[137,17842,1426],{"class":143},[137,17844,14329],{"class":147},[137,17846,924],{"class":157},[137,17848,17849],{"class":139,"line":3861},[137,17850,1966],{"class":157},[137,17852,17853],{"class":139,"line":3883},[137,17854,294],{"class":157},[137,17856,17857],{"class":139,"line":3896},[137,17858,510],{"class":157},[27,17860,14348,17861,13801,17863,14354,17865,14358],{},[22,17862,13088],{},[22,17864,14353],{},[22,17866,14357],{},[128,17868,17870],{"className":13299,"code":17869,"language":13301,"meta":133,"style":133},"import { applyDecorators, UseGuards, SetMetadata } from \"@nestjs\u002Fcommon\";\nimport { AuthGuard } from \"..\u002Fguards\u002Fauth.guard\";\n\nexport function Auth(...permissions: string[]) {\n    return applyDecorators(SetMetadata(\"permissions\", permissions), UseGuards(AuthGuard));\n}\n",[22,17871,17872,17884,17897,17901,17921,17942],{"__ignoreMap":133},[137,17873,17874,17876,17878,17880,17882],{"class":139,"line":140},[137,17875,10287],{"class":143},[137,17877,14370],{"class":157},[137,17879,10954],{"class":143},[137,17881,13315],{"class":284},[137,17883,3276],{"class":157},[137,17885,17886,17888,17891,17893,17895],{"class":139,"line":173},[137,17887,10287],{"class":143},[137,17889,17890],{"class":157}," { AuthGuard } ",[137,17892,10954],{"class":143},[137,17894,14388],{"class":284},[137,17896,3276],{"class":157},[137,17898,17899],{"class":139,"line":188},[137,17900,516],{"emptyLinePlaceholder":515},[137,17902,17903,17905,17907,17909,17911,17913,17915,17917,17919],{"class":139,"line":269},[137,17904,13456],{"class":143},[137,17906,154],{"class":143},[137,17908,14403],{"class":147},[137,17910,356],{"class":157},[137,17912,14408],{"class":143},[137,17914,14019],{"class":161},[137,17916,894],{"class":143},[137,17918,13630],{"class":364},[137,17920,14417],{"class":157},[137,17922,17923,17925,17927,17929,17931,17933,17935,17937,17939],{"class":139,"line":278},[137,17924,176],{"class":143},[137,17926,14424],{"class":147},[137,17928,356],{"class":157},[137,17930,14429],{"class":147},[137,17932,356],{"class":157},[137,17934,14164],{"class":284},[137,17936,14436],{"class":157},[137,17938,14439],{"class":147},[137,17940,17941],{"class":157},"(AuthGuard));\n",[137,17943,17944],{"class":139,"line":291},[137,17945,510],{"class":157},[27,17947,17948],{},"And that’s it. I will demonstrate how we can use this custom decorator to protect our endpoints later in this article.",[27,17950,17951],{},"We will now implement the user creation using the Firebase Admin SDK.",[104,17953,17955],{"id":17954},"user-creation-with-firebase-admin-sdk","User Creation with Firebase Admin SDK",[27,17957,17958],{},"Before we jump to the implementation on the user creation, a couple of things needs to be done beforehand. Firstly we need to enable the email and password authentication in the Firebase console in our project. Secondly, create the Data Transfer Object (DTO) file where will define the user properties that will be validated on incoming requests.",[123,17960,17962],{"id":17961},"enable-emailpassword-authentication","Enable Email\u002FPassword Authentication",[27,17964,17965],{},"To enable Email\u002FAuthentication, navigate to the Authentication module → Email\u002FPassword.",[27,17967,17968],{},[63,17969],{"alt":17970,"src":17971},"Enabling Email Authentication.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1665124675\u002Fblog\u002Fnestjs-authorisation-with-firebase-auth\u002Fnestjs-authorisation-with-firebase-auth_allplx",[27,17973,17974],{},"Next, enable Email\u002FPassword and save it.",[27,17976,17977],{},[63,17978],{"alt":17970,"src":17979},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fc_scale,f_auto,w_750\u002Fv1665124799\u002Fblog\u002Fnestjs-authorisation-with-firebase-auth\u002Fnestjs-authorisation-with-firebase-auth_pxoibq",[27,17981,17982],{},"We are now able sign up\u002Fsign in users using the Firebase Auth with email and password.",[123,17984,14521],{"id":14520},[27,17986,14524,17987,14528,17989,13818],{},[22,17988,14527],{},[22,17990,14531],{},[128,17992,17993],{"className":8665,"code":14534,"language":8667,"meta":133,"style":133},[22,17994,17995],{"__ignoreMap":133},[137,17996,17997,17999,18001,18003,18005],{"class":139,"line":140},[137,17998,9536],{"class":147},[137,18000,10268],{"class":284},[137,18002,14545],{"class":284},[137,18004,14548],{"class":284},[137,18006,14551],{"class":364},[27,18008,14554,18009,9335,18011,14561,18013,14564,18015,14568,18017,14571],{},[22,18010,14557],{},[22,18012,14560],{},[22,18014,14557],{},[22,18016,14567],{},[22,18018,14567],{},[128,18020,18021],{"className":13299,"code":14574,"language":13301,"meta":133,"style":133},[22,18022,18023,18035,18039,18047,18057,18067,18077,18081,18085,18095,18103,18111,18121,18125,18133,18145,18157,18211,18219,18223,18233,18237,18245,18257,18269,18277,18287,18291,18299,18311,18323,18331,18341,18345,18353,18365,18375],{"__ignoreMap":133},[137,18024,18025,18027,18029,18031,18033],{"class":139,"line":140},[137,18026,10287],{"class":143},[137,18028,14583],{"class":157},[137,18030,10954],{"class":143},[137,18032,14588],{"class":284},[137,18034,3276],{"class":157},[137,18036,18037],{"class":139,"line":173},[137,18038,516],{"emptyLinePlaceholder":515},[137,18040,18041,18043,18045],{"class":139,"line":188},[137,18042,14599],{"class":143},[137,18044,14602],{"class":147},[137,18046,256],{"class":157},[137,18048,18049,18051,18053,18055],{"class":139,"line":269},[137,18050,14609],{"class":364},[137,18052,151],{"class":143},[137,18054,14614],{"class":284},[137,18056,1961],{"class":157},[137,18058,18059,18061,18063,18065],{"class":139,"line":278},[137,18060,14621],{"class":364},[137,18062,151],{"class":143},[137,18064,14626],{"class":284},[137,18066,1961],{"class":157},[137,18068,18069,18071,18073,18075],{"class":139,"line":291},[137,18070,14633],{"class":364},[137,18072,151],{"class":143},[137,18074,14638],{"class":284},[137,18076,1961],{"class":157},[137,18078,18079],{"class":139,"line":297},[137,18080,510],{"class":157},[137,18082,18083],{"class":139,"line":302},[137,18084,516],{"emptyLinePlaceholder":515},[137,18086,18087,18089,18091,18093],{"class":139,"line":662},[137,18088,13456],{"class":143},[137,18090,7832],{"class":143},[137,18092,14657],{"class":147},[137,18094,256],{"class":157},[137,18096,18097,18099,18101],{"class":139,"line":667},[137,18098,14664],{"class":157},[137,18100,14667],{"class":147},[137,18102,2754],{"class":157},[137,18104,18105,18107,18109],{"class":139,"line":786},[137,18106,14664],{"class":157},[137,18108,14676],{"class":147},[137,18110,2754],{"class":157},[137,18112,18113,18115,18117,18119],{"class":139,"line":798},[137,18114,14683],{"class":161},[137,18116,894],{"class":143},[137,18118,13630],{"class":364},[137,18120,3276],{"class":157},[137,18122,18123],{"class":139,"line":803},[137,18124,516],{"emptyLinePlaceholder":515},[137,18126,18127,18129,18131],{"class":139,"line":931},[137,18128,14664],{"class":157},[137,18130,14676],{"class":147},[137,18132,2754],{"class":157},[137,18134,18135,18137,18139,18141,18143],{"class":139,"line":1568},[137,18136,14664],{"class":157},[137,18138,14708],{"class":147},[137,18140,356],{"class":157},[137,18142,14713],{"class":364},[137,18144,3155],{"class":157},[137,18146,18147,18149,18151,18153,18155],{"class":139,"line":1573},[137,18148,14664],{"class":157},[137,18150,14722],{"class":147},[137,18152,356],{"class":157},[137,18154,14727],{"class":364},[137,18156,3155],{"class":157},[137,18158,18159,18161,18163,18165,18167,18169,18171,18173,18175,18177,18179,18181,18183,18185,18187,18189,18191,18193,18195,18197,18199,18201,18203,18205,18207,18209],{"class":139,"line":1578},[137,18160,14664],{"class":157},[137,18162,14736],{"class":147},[137,18164,356],{"class":157},[137,18166,47],{"class":284},[137,18168,14743],{"class":143},[137,18170,14747],{"class":14746},[137,18172,1017],{"class":364},[137,18174,7672],{"class":143},[137,18176,14754],{"class":364},[137,18178,14757],{"class":14746},[137,18180,1017],{"class":364},[137,18182,7672],{"class":143},[137,18184,14764],{"class":364},[137,18186,14757],{"class":14746},[137,18188,1017],{"class":364},[137,18190,7672],{"class":143},[137,18192,14773],{"class":364},[137,18194,14757],{"class":14746},[137,18196,1017],{"class":364},[137,18198,7672],{"class":143},[137,18200,14782],{"class":364},[137,18202,14105],{"class":14746},[137,18204,1017],{"class":364},[137,18206,14789],{"class":143},[137,18208,47],{"class":284},[137,18210,5396],{"class":157},[137,18212,18213,18215,18217],{"class":139,"line":1588},[137,18214,14798],{"class":157},[137,18216,14801],{"class":284},[137,18218,1961],{"class":157},[137,18220,18221],{"class":139,"line":1601},[137,18222,2800],{"class":157},[137,18224,18225,18227,18229,18231],{"class":139,"line":3802},[137,18226,14812],{"class":161},[137,18228,894],{"class":143},[137,18230,13630],{"class":364},[137,18232,3276],{"class":157},[137,18234,18235],{"class":139,"line":3808},[137,18236,516],{"emptyLinePlaceholder":515},[137,18238,18239,18241,18243],{"class":139,"line":3822},[137,18240,14664],{"class":157},[137,18242,14676],{"class":147},[137,18244,2754],{"class":157},[137,18246,18247,18249,18251,18253,18255],{"class":139,"line":3827},[137,18248,14664],{"class":157},[137,18250,14708],{"class":147},[137,18252,356],{"class":157},[137,18254,10345],{"class":364},[137,18256,3155],{"class":157},[137,18258,18259,18261,18263,18265,18267],{"class":139,"line":3832},[137,18260,14664],{"class":157},[137,18262,14722],{"class":147},[137,18264,356],{"class":157},[137,18266,14727],{"class":364},[137,18268,3155],{"class":157},[137,18270,18271,18273,18275],{"class":139,"line":3840},[137,18272,14664],{"class":157},[137,18274,14861],{"class":147},[137,18276,2754],{"class":157},[137,18278,18279,18281,18283,18285],{"class":139,"line":3846},[137,18280,14868],{"class":161},[137,18282,894],{"class":143},[137,18284,13630],{"class":364},[137,18286,3276],{"class":157},[137,18288,18289],{"class":139,"line":3861},[137,18290,516],{"emptyLinePlaceholder":515},[137,18292,18293,18295,18297],{"class":139,"line":3883},[137,18294,14664],{"class":157},[137,18296,14676],{"class":147},[137,18298,2754],{"class":157},[137,18300,18301,18303,18305,18307,18309],{"class":139,"line":3896},[137,18302,14664],{"class":157},[137,18304,14708],{"class":147},[137,18306,356],{"class":157},[137,18308,10345],{"class":364},[137,18310,3155],{"class":157},[137,18312,18313,18315,18317,18319,18321],{"class":139,"line":3901},[137,18314,14664],{"class":157},[137,18316,14722],{"class":147},[137,18318,356],{"class":157},[137,18320,14727],{"class":364},[137,18322,3155],{"class":157},[137,18324,18325,18327,18329],{"class":139,"line":3906},[137,18326,14664],{"class":157},[137,18328,14861],{"class":147},[137,18330,2754],{"class":157},[137,18332,18333,18335,18337,18339],{"class":139,"line":3911},[137,18334,14923],{"class":161},[137,18336,894],{"class":143},[137,18338,13630],{"class":364},[137,18340,3276],{"class":157},[137,18342,18343],{"class":139,"line":4666},[137,18344,516],{"emptyLinePlaceholder":515},[137,18346,18347,18349,18351],{"class":139,"line":4672},[137,18348,14664],{"class":157},[137,18350,14676],{"class":147},[137,18352,2754],{"class":157},[137,18354,18355,18357,18359,18361,18363],{"class":139,"line":4680},[137,18356,14664],{"class":157},[137,18358,14948],{"class":147},[137,18360,14951],{"class":157},[137,18362,3097],{"class":364},[137,18364,14956],{"class":157},[137,18366,18367,18369,18371,18373],{"class":139,"line":4711},[137,18368,14961],{"class":161},[137,18370,894],{"class":143},[137,18372,14602],{"class":147},[137,18374,14968],{"class":157},[137,18376,18377],{"class":139,"line":4716},[137,18378,510],{"class":157},[27,18380,18381,18382,164,18384,114,18386,1017],{},"We should expect five properties to be sent with the request: an email address, a strong password, users first and last name and user’s permissions (roles) which will accept three possible values, ",[22,18383,14978],{},[22,18385,14981],{},[22,18387,14984],{},[27,18389,14987,18390,2107,18392,14993,18394,14996],{},[22,18391,14990],{},[22,18393,13114],{},[22,18395,13114],{},[128,18397,18398],{"className":13299,"code":14999,"language":13301,"meta":133,"style":133},[22,18399,18400,18412,18424,18436,18440,18450,18466,18480,18494,18498],{"__ignoreMap":133},[137,18401,18402,18404,18406,18408,18410],{"class":139,"line":140},[137,18403,10287],{"class":143},[137,18405,15008],{"class":157},[137,18407,10954],{"class":143},[137,18409,13917],{"class":284},[137,18411,3276],{"class":157},[137,18413,18414,18416,18418,18420,18422],{"class":139,"line":173},[137,18415,10287],{"class":143},[137,18417,15021],{"class":157},[137,18419,10954],{"class":143},[137,18421,15026],{"class":284},[137,18423,3276],{"class":157},[137,18425,18426,18428,18430,18432,18434],{"class":139,"line":188},[137,18427,10287],{"class":143},[137,18429,15035],{"class":157},[137,18431,10954],{"class":143},[137,18433,13315],{"class":284},[137,18435,3276],{"class":157},[137,18437,18438],{"class":139,"line":269},[137,18439,516],{"emptyLinePlaceholder":515},[137,18441,18442,18444,18446,18448],{"class":139,"line":278},[137,18443,15050],{"class":143},[137,18445,154],{"class":143},[137,18447,15055],{"class":147},[137,18449,275],{"class":157},[137,18451,18452,18454,18456,18458,18460,18462,18464],{"class":139,"line":291},[137,18453,4177],{"class":143},[137,18455,15064],{"class":364},[137,18457,151],{"class":143},[137,18459,15069],{"class":143},[137,18461,15072],{"class":157},[137,18463,15075],{"class":147},[137,18465,15078],{"class":157},[137,18467,18468,18470,18472,18474,18476,18478],{"class":139,"line":297},[137,18469,15083],{"class":157},[137,18471,15086],{"class":147},[137,18473,356],{"class":157},[137,18475,1361],{"class":143},[137,18477,15093],{"class":147},[137,18479,14173],{"class":157},[137,18481,18482,18484,18486,18488,18490,18492],{"class":139,"line":302},[137,18483,15100],{"class":143},[137,18485,15103],{"class":157},[137,18487,15106],{"class":147},[137,18489,356],{"class":157},[137,18491,15111],{"class":364},[137,18493,1502],{"class":157},[137,18495,18496],{"class":139,"line":662},[137,18497,510],{"class":157},[137,18499,18500,18502],{"class":139,"line":667},[137,18501,15122],{"class":147},[137,18503,924],{"class":157},[123,18505,15128],{"id":15127},[27,18507,18508,18509,18511,18512,15451],{},"Before jumping into the user creation implementation, the",[22,18510,17275],{}," file needs to be imported and declared as a provider in our user’s module. So our ",[22,18513,13189],{},[128,18515,18517],{"className":13299,"code":18516,"language":13301,"meta":133,"style":133},"import { Module } from \"@nestjs\u002Fcommon\";\nimport { FirebaseAdmin } from \"config\u002Ffirebase.setup\";\nimport { UserController } from \".\u002Fuser.controller\";\nimport { UserService } from \".\u002Fuser.service\";\n\n@Module({\n    controllers: [UserController],\n    providers: [UserService, FirebaseAdmin],\n})\nexport class UserModule {}\n",[22,18518,18519,18531,18544,18556,18568,18572,18580,18584,18589,18593],{"__ignoreMap":133},[137,18520,18521,18523,18525,18527,18529],{"class":139,"line":140},[137,18522,10287],{"class":143},[137,18524,13310],{"class":157},[137,18526,10954],{"class":143},[137,18528,13315],{"class":284},[137,18530,3276],{"class":157},[137,18532,18533,18535,18537,18539,18542],{"class":139,"line":173},[137,18534,10287],{"class":143},[137,18536,17562],{"class":157},[137,18538,10954],{"class":143},[137,18540,18541],{"class":284}," \"config\u002Ffirebase.setup\"",[137,18543,3276],{"class":157},[137,18545,18546,18548,18550,18552,18554],{"class":139,"line":188},[137,18547,10287],{"class":143},[137,18549,15355],{"class":157},[137,18551,10954],{"class":143},[137,18553,15360],{"class":284},[137,18555,3276],{"class":157},[137,18557,18558,18560,18562,18564,18566],{"class":139,"line":269},[137,18559,10287],{"class":143},[137,18561,15341],{"class":157},[137,18563,10954],{"class":143},[137,18565,15346],{"class":284},[137,18567,3276],{"class":157},[137,18569,18570],{"class":139,"line":278},[137,18571,516],{"emptyLinePlaceholder":515},[137,18573,18574,18576,18578],{"class":139,"line":291},[137,18575,13382],{"class":157},[137,18577,13385],{"class":147},[137,18579,3175],{"class":157},[137,18581,18582],{"class":139,"line":297},[137,18583,15416],{"class":157},[137,18585,18586],{"class":139,"line":302},[137,18587,18588],{"class":157},"    providers: [UserService, FirebaseAdmin],\n",[137,18590,18591],{"class":139,"line":662},[137,18592,13451],{"class":157},[137,18594,18595,18597,18599,18601],{"class":139,"line":667},[137,18596,13456],{"class":143},[137,18598,7832],{"class":143},[137,18600,15434],{"class":147},[137,18602,13464],{"class":157},[27,18604,18605,18606,15443,18608,15447,18610,15451],{},"We can now use Firebase setup in the user’s service. Head over to the user’s service file and create a method called ",[22,18607,15442],{},[22,18609,15446],{},[22,18611,15450],{},[128,18613,18615],{"className":13299,"code":18614,"language":13301,"meta":133,"style":133},"import { Injectable, BadRequestException } from \"@nestjs\u002Fcommon\";\nimport { FirebaseAdmin } from \"..\u002F..\u002Fconfig\u002Ffirebase.setup\";\nimport { UserDto } from \".\u002Fdto\u002Fuser.dto\";\n\n@Injectable()\nexport class UserService {\n    constructor(private readonly admin: FirebaseAdmin) {}\n\n    async createUser(userRequest: UserDto): Promise\u003Cany> {\n        const { email, password, firstName, lastName, role } = userRequest;\n        const app = this.admin.setup();\n\n        try {\n            const createdUser = await app.auth().createUser({\n                email,\n                password,\n                displayName: `${firstName} ${lastName}`,\n            });\n            await app.auth().setCustomUserClaims(createdUser.uid, { role });\n            return createdUser;\n        } catch (error) {\n            throw new BadRequestException(error.message);\n        }\n    }\n}\n",[22,18616,18617,18630,18642,18654,18658,18666,18676,18694,18698,18725,18756,18772,18776,18782,18803,18808,18813,18831,18835,18852,18859,18867,18877,18881,18885],{"__ignoreMap":133},[137,18618,18619,18621,18624,18626,18628],{"class":139,"line":140},[137,18620,10287],{"class":143},[137,18622,18623],{"class":157}," { Injectable, BadRequestException } ",[137,18625,10954],{"class":143},[137,18627,13315],{"class":284},[137,18629,3276],{"class":157},[137,18631,18632,18634,18636,18638,18640],{"class":139,"line":173},[137,18633,10287],{"class":143},[137,18635,17562],{"class":157},[137,18637,10954],{"class":143},[137,18639,17567],{"class":284},[137,18641,3276],{"class":157},[137,18643,18644,18646,18648,18650,18652],{"class":139,"line":188},[137,18645,10287],{"class":143},[137,18647,15476],{"class":157},[137,18649,10954],{"class":143},[137,18651,15481],{"class":284},[137,18653,3276],{"class":157},[137,18655,18656],{"class":139,"line":269},[137,18657,516],{"emptyLinePlaceholder":515},[137,18659,18660,18662,18664],{"class":139,"line":278},[137,18661,13382],{"class":157},[137,18663,13604],{"class":147},[137,18665,2754],{"class":157},[137,18667,18668,18670,18672,18674],{"class":139,"line":291},[137,18669,13456],{"class":143},[137,18671,7832],{"class":143},[137,18673,15516],{"class":147},[137,18675,256],{"class":157},[137,18677,18678,18680,18682,18684,18686,18688,18690,18692],{"class":139,"line":297},[137,18679,3651],{"class":143},[137,18681,356],{"class":157},[137,18683,15234],{"class":143},[137,18685,13992],{"class":143},[137,18687,17336],{"class":161},[137,18689,894],{"class":143},[137,18691,17368],{"class":147},[137,18693,15246],{"class":157},[137,18695,18696],{"class":139,"line":302},[137,18697,516],{"emptyLinePlaceholder":515},[137,18699,18700,18702,18704,18706,18708,18710,18712,18714,18716,18718,18720,18723],{"class":139,"line":662},[137,18701,15546],{"class":143},[137,18703,15549],{"class":147},[137,18705,356],{"class":157},[137,18707,15554],{"class":161},[137,18709,894],{"class":143},[137,18711,14657],{"class":147},[137,18713,14105],{"class":157},[137,18715,894],{"class":143},[137,18717,14116],{"class":147},[137,18719,4033],{"class":157},[137,18721,18722],{"class":364},"any",[137,18724,14136],{"class":157},[137,18726,18727,18729,18731,18733,18735,18737,18739,18741,18743,18745,18747,18750,18752,18754],{"class":139,"line":667},[137,18728,3008],{"class":143},[137,18730,8906],{"class":157},[137,18732,15569],{"class":364},[137,18734,164],{"class":157},[137,18736,15574],{"class":364},[137,18738,164],{"class":157},[137,18740,4693],{"class":364},[137,18742,164],{"class":157},[137,18744,4703],{"class":364},[137,18746,164],{"class":157},[137,18748,18749],{"class":364},"role",[137,18751,8911],{"class":157},[137,18753,253],{"class":143},[137,18755,15593],{"class":157},[137,18757,18758,18760,18762,18764,18766,18768,18770],{"class":139,"line":786},[137,18759,3008],{"class":143},[137,18761,15064],{"class":364},[137,18763,151],{"class":143},[137,18765,365],{"class":364},[137,18767,17675],{"class":157},[137,18769,15609],{"class":147},[137,18771,924],{"class":157},[137,18773,18774],{"class":139,"line":798},[137,18775,516],{"emptyLinePlaceholder":515},[137,18777,18778,18780],{"class":139,"line":803},[137,18779,15697],{"class":143},[137,18781,256],{"class":157},[137,18783,18784,18786,18789,18791,18793,18795,18797,18799,18801],{"class":139,"line":931},[137,18785,5772],{"class":143},[137,18787,18788],{"class":364}," createdUser",[137,18790,151],{"class":143},[137,18792,15069],{"class":143},[137,18794,15103],{"class":157},[137,18796,2751],{"class":147},[137,18798,17766],{"class":157},[137,18800,15442],{"class":147},[137,18802,3175],{"class":157},[137,18804,18805],{"class":139,"line":1568},[137,18806,18807],{"class":157},"                email,\n",[137,18809,18810],{"class":139,"line":1573},[137,18811,18812],{"class":157},"                password,\n",[137,18814,18815,18818,18821,18823,18825,18827,18829],{"class":139,"line":1578},[137,18816,18817],{"class":157},"                displayName: ",[137,18819,18820],{"class":284},"`${",[137,18822,4693],{"class":157},[137,18824,4696],{"class":284},[137,18826,4703],{"class":157},[137,18828,4706],{"class":284},[137,18830,1961],{"class":157},[137,18832,18833],{"class":139,"line":1588},[137,18834,14336],{"class":157},[137,18836,18837,18840,18842,18844,18846,18849],{"class":139,"line":1601},[137,18838,18839],{"class":143},"            await",[137,18841,15103],{"class":157},[137,18843,2751],{"class":147},[137,18845,17766],{"class":157},[137,18847,18848],{"class":147},"setCustomUserClaims",[137,18850,18851],{"class":157},"(createdUser.uid, { role });\n",[137,18853,18854,18856],{"class":139,"line":3802},[137,18855,4683],{"class":143},[137,18857,18858],{"class":157}," createdUser;\n",[137,18860,18861,18863,18865],{"class":139,"line":3808},[137,18862,15729],{"class":157},[137,18864,2807],{"class":143},[137,18866,15734],{"class":157},[137,18868,18869,18871,18873,18875],{"class":139,"line":3822},[137,18870,15739],{"class":143},[137,18872,1426],{"class":143},[137,18874,15744],{"class":147},[137,18876,15747],{"class":157},[137,18878,18879],{"class":139,"line":3827},[137,18880,1966],{"class":157},[137,18882,18883],{"class":139,"line":3832},[137,18884,294],{"class":157},[137,18886,18887],{"class":139,"line":3840},[137,18888,510],{"class":157},[27,18890,18891,18892,14996],{},"We can make a use of this module inside user’s controller where we will now declare our endpoint. Our ",[22,18893,15765],{},[128,18895,18896],{"className":13299,"code":15768,"language":13301,"meta":133,"style":133},[22,18897,18898,18910,18922,18934,18938,18950,18960,18978,18982,18994,19012,19024,19028],{"__ignoreMap":133},[137,18899,18900,18902,18904,18906,18908],{"class":139,"line":140},[137,18901,10287],{"class":143},[137,18903,15777],{"class":157},[137,18905,10954],{"class":143},[137,18907,13315],{"class":284},[137,18909,3276],{"class":157},[137,18911,18912,18914,18916,18918,18920],{"class":139,"line":173},[137,18913,10287],{"class":143},[137,18915,15341],{"class":157},[137,18917,10954],{"class":143},[137,18919,15346],{"class":284},[137,18921,3276],{"class":157},[137,18923,18924,18926,18928,18930,18932],{"class":139,"line":188},[137,18925,10287],{"class":143},[137,18927,15476],{"class":157},[137,18929,10954],{"class":143},[137,18931,15481],{"class":284},[137,18933,3276],{"class":157},[137,18935,18936],{"class":139,"line":269},[137,18937,516],{"emptyLinePlaceholder":515},[137,18939,18940,18942,18944,18946,18948],{"class":139,"line":278},[137,18941,13382],{"class":157},[137,18943,15818],{"class":147},[137,18945,356],{"class":157},[137,18947,15823],{"class":284},[137,18949,3155],{"class":157},[137,18951,18952,18954,18956,18958],{"class":139,"line":291},[137,18953,13456],{"class":143},[137,18955,7832],{"class":143},[137,18957,15834],{"class":147},[137,18959,256],{"class":157},[137,18961,18962,18964,18966,18968,18970,18972,18974,18976],{"class":139,"line":297},[137,18963,3651],{"class":143},[137,18965,356],{"class":157},[137,18967,15234],{"class":143},[137,18969,13992],{"class":143},[137,18971,15849],{"class":161},[137,18973,894],{"class":143},[137,18975,15516],{"class":147},[137,18977,15246],{"class":157},[137,18979,18980],{"class":139,"line":302},[137,18981,516],{"emptyLinePlaceholder":515},[137,18983,18984,18986,18988,18990,18992],{"class":139,"line":662},[137,18985,14664],{"class":157},[137,18987,15866],{"class":147},[137,18989,356],{"class":157},[137,18991,15871],{"class":284},[137,18993,3155],{"class":157},[137,18995,18996,18998,19000,19002,19004,19006,19008,19010],{"class":139,"line":667},[137,18997,15878],{"class":147},[137,18999,15881],{"class":157},[137,19001,15884],{"class":147},[137,19003,3348],{"class":157},[137,19005,15554],{"class":161},[137,19007,894],{"class":143},[137,19009,14657],{"class":147},[137,19011,170],{"class":157},[137,19013,19014,19016,19018,19020,19022],{"class":139,"line":786},[137,19015,5472],{"class":143},[137,19017,365],{"class":364},[137,19019,15903],{"class":157},[137,19021,15442],{"class":147},[137,19023,15908],{"class":157},[137,19025,19026],{"class":139,"line":798},[137,19027,294],{"class":157},[137,19029,19030],{"class":139,"line":803},[137,19031,510],{"class":157},[27,19033,19034],{},"And that’s it. We will next create our first user.",[104,19036,15923],{"id":15922},[27,19038,15926,19039,15932,19042,15938,19045,15942],{},[45,19040,15931],{"href":15929,"target":2716,"rel":19041},[2718,2719],[45,19043,15937],{"href":15935,"target":2716,"rel":19044},[2718,2719],[22,19046,15941],{},[27,19048,15945],{},[128,19050,19053],{"className":19051,"code":19052,"language":5189},[5187],"POST http:\u002F\u002Flocalhost:3000\u002Fuser\u002Fsignup HTTP\u002F1.1\nContent-Type: application\u002Fjson\n\n{\n    \"email\": \"admin@test.com\",\n    \"password\": \"P@ssword1\",\n    \"firstName\": \"Darrell\",\n    \"lastName\": \"Werner\",\n    \"role\": \"ADMIN\"\n}\n",[22,19054,19052],{"__ignoreMap":133},[27,19056,19057,19058,19060],{},"With the above request, we have created a User with ",[22,19059,14978],{}," role.",[27,19062,19063,19064,114,19066,19068],{},"Let’s now proceed to create two more users, but with different roles: ",[22,19065,14981],{},[22,19067,14984],{},". If we try to create a user with permissions that we have’t defined in our DTO enum Permissions, we will get an error:",[128,19070,19072],{"className":19071,"code":16019,"language":5189},[5187],[22,19073,16019],{"__ignoreMap":133},[104,19075,16067],{"id":16066},[27,19077,16070,19078,16074,19081,16077,19083,16080],{},[22,19079,19080],{},"FirebaseAdmin",[22,19082,13104],{},[22,19084,13104],{},[128,19086,19088],{"className":13299,"code":19087,"language":13301,"meta":133,"style":133},"import { Module } from \"@nestjs\u002Fcommon\";\nimport { AppController } from \".\u002Fapp.controller\";\nimport { AppService } from \".\u002Fapp.service\";\nimport { ConfigModule } from \"@nestjs\u002Fconfig\";\nimport { FirebaseAdmin } from \"..\u002Fconfig\u002Ffirebase.setup\";\nimport { UserModule } from \".\u002Fuser\u002Fuser.module\";\n\n@Module({\n    imports: [\n        ConfigModule.forRoot({\n            isGlobal: true,\n            envFilePath: `.env`,\n        }),\n        UserModule,\n    ],\n    controllers: [AppController],\n    providers: [AppService, FirebaseAdmin],\n})\nexport class AppModule {}\n",[22,19089,19090,19102,19114,19126,19138,19151,19163,19167,19175,19179,19187,19195,19204,19208,19212,19216,19220,19225,19229],{"__ignoreMap":133},[137,19091,19092,19094,19096,19098,19100],{"class":139,"line":140},[137,19093,10287],{"class":143},[137,19095,13310],{"class":157},[137,19097,10954],{"class":143},[137,19099,13315],{"class":284},[137,19101,3276],{"class":157},[137,19103,19104,19106,19108,19110,19112],{"class":139,"line":173},[137,19105,10287],{"class":143},[137,19107,13324],{"class":157},[137,19109,10954],{"class":143},[137,19111,13329],{"class":284},[137,19113,3276],{"class":157},[137,19115,19116,19118,19120,19122,19124],{"class":139,"line":188},[137,19117,10287],{"class":143},[137,19119,13338],{"class":157},[137,19121,10954],{"class":143},[137,19123,13343],{"class":284},[137,19125,3276],{"class":157},[137,19127,19128,19130,19132,19134,19136],{"class":139,"line":269},[137,19129,10287],{"class":143},[137,19131,13366],{"class":157},[137,19133,10954],{"class":143},[137,19135,13371],{"class":284},[137,19137,3276],{"class":157},[137,19139,19140,19142,19144,19146,19149],{"class":139,"line":278},[137,19141,10287],{"class":143},[137,19143,17562],{"class":157},[137,19145,10954],{"class":143},[137,19147,19148],{"class":284}," \"..\u002Fconfig\u002Ffirebase.setup\"",[137,19150,3276],{"class":157},[137,19152,19153,19155,19157,19159,19161],{"class":139,"line":291},[137,19154,10287],{"class":143},[137,19156,13352],{"class":157},[137,19158,10954],{"class":143},[137,19160,13357],{"class":284},[137,19162,3276],{"class":157},[137,19164,19165],{"class":139,"line":297},[137,19166,516],{"emptyLinePlaceholder":515},[137,19168,19169,19171,19173],{"class":139,"line":302},[137,19170,13382],{"class":157},[137,19172,13385],{"class":147},[137,19174,3175],{"class":157},[137,19176,19177],{"class":139,"line":662},[137,19178,13392],{"class":157},[137,19180,19181,19183,19185],{"class":139,"line":667},[137,19182,13397],{"class":157},[137,19184,13400],{"class":147},[137,19186,3175],{"class":157},[137,19188,19189,19191,19193],{"class":139,"line":786},[137,19190,13407],{"class":157},[137,19192,3097],{"class":364},[137,19194,1961],{"class":157},[137,19196,19197,19199,19202],{"class":139,"line":798},[137,19198,13416],{"class":157},[137,19200,19201],{"class":284},"`.env`",[137,19203,1961],{"class":157},[137,19205,19206],{"class":139,"line":803},[137,19207,13426],{"class":157},[137,19209,19210],{"class":139,"line":931},[137,19211,13431],{"class":157},[137,19213,19214],{"class":139,"line":1568},[137,19215,13436],{"class":157},[137,19217,19218],{"class":139,"line":1573},[137,19219,13441],{"class":157},[137,19221,19222],{"class":139,"line":1578},[137,19223,19224],{"class":157},"    providers: [AppService, FirebaseAdmin],\n",[137,19226,19227],{"class":139,"line":1588},[137,19228,13451],{"class":157},[137,19230,19231,19233,19235,19237],{"class":139,"line":1601},[137,19232,13456],{"class":143},[137,19234,7832],{"class":143},[137,19236,13461],{"class":147},[137,19238,13464],{"class":157},[27,19240,16224],{},[2569,19242,19243,19249,19255],{},[1006,19244,19245,16232,19247,16237],{},[22,19246,16231],{},[22,19248,14978],{},[1006,19250,19251,16243,19253,16246],{},[22,19252,16242],{},[22,19254,14984],{},[1006,19256,19257,16252],{},[22,19258,16251],{},[27,19260,16255,19261,14571],{},[22,19262,13099],{},[128,19264,19266],{"className":13299,"code":19265,"language":13301,"meta":133,"style":133},"import { Controller, Get } from \"@nestjs\u002Fcommon\";\nimport { Auth } from \".\u002Fdecorators\u002Fauth.decorator\";\n\n@Controller()\nexport class AppController {\n    @Get(\"\u002Fmorning\")\n    @Auth(\"ADMIN\")\n    goodMorning() {\n        return \"Good Morning!\";\n    }\n\n    @Get(\"\u002Fafternoon\")\n    @Auth(\"DEVELOPER\")\n    goodAfternoon() {\n        return \"Good Afternoon!\";\n    }\n\n    @Get(\"\u002Fevening\")\n    goodEvening() {\n        return \"Good Evening!\";\n    }\n}\n",[22,19267,19268,19280,19292,19296,19304,19314,19326,19338,19344,19352,19356,19360,19372,19384,19390,19398,19402,19406,19418,19424,19432,19436],{"__ignoreMap":133},[137,19269,19270,19272,19274,19276,19278],{"class":139,"line":140},[137,19271,10287],{"class":143},[137,19273,16269],{"class":157},[137,19275,10954],{"class":143},[137,19277,13315],{"class":284},[137,19279,3276],{"class":157},[137,19281,19282,19284,19286,19288,19290],{"class":139,"line":173},[137,19283,10287],{"class":143},[137,19285,16282],{"class":157},[137,19287,10954],{"class":143},[137,19289,16287],{"class":284},[137,19291,3276],{"class":157},[137,19293,19294],{"class":139,"line":188},[137,19295,516],{"emptyLinePlaceholder":515},[137,19297,19298,19300,19302],{"class":139,"line":269},[137,19299,13382],{"class":157},[137,19301,15818],{"class":147},[137,19303,2754],{"class":157},[137,19305,19306,19308,19310,19312],{"class":139,"line":278},[137,19307,13456],{"class":143},[137,19309,7832],{"class":143},[137,19311,16310],{"class":147},[137,19313,256],{"class":157},[137,19315,19316,19318,19320,19322,19324],{"class":139,"line":291},[137,19317,14664],{"class":157},[137,19319,16319],{"class":147},[137,19321,356],{"class":157},[137,19323,16324],{"class":284},[137,19325,3155],{"class":157},[137,19327,19328,19330,19332,19334,19336],{"class":139,"line":297},[137,19329,14664],{"class":157},[137,19331,16333],{"class":147},[137,19333,356],{"class":157},[137,19335,16338],{"class":284},[137,19337,3155],{"class":157},[137,19339,19340,19342],{"class":139,"line":302},[137,19341,16350],{"class":147},[137,19343,275],{"class":157},[137,19345,19346,19348,19350],{"class":139,"line":662},[137,19347,5472],{"class":143},[137,19349,16359],{"class":284},[137,19351,3276],{"class":157},[137,19353,19354],{"class":139,"line":667},[137,19355,294],{"class":157},[137,19357,19358],{"class":139,"line":786},[137,19359,516],{"emptyLinePlaceholder":515},[137,19361,19362,19364,19366,19368,19370],{"class":139,"line":798},[137,19363,14664],{"class":157},[137,19365,16319],{"class":147},[137,19367,356],{"class":157},[137,19369,16380],{"class":284},[137,19371,3155],{"class":157},[137,19373,19374,19376,19378,19380,19382],{"class":139,"line":803},[137,19375,14664],{"class":157},[137,19377,16333],{"class":147},[137,19379,356],{"class":157},[137,19381,16393],{"class":284},[137,19383,3155],{"class":157},[137,19385,19386,19388],{"class":139,"line":931},[137,19387,16400],{"class":147},[137,19389,275],{"class":157},[137,19391,19392,19394,19396],{"class":139,"line":1568},[137,19393,5472],{"class":143},[137,19395,16409],{"class":284},[137,19397,3276],{"class":157},[137,19399,19400],{"class":139,"line":1573},[137,19401,294],{"class":157},[137,19403,19404],{"class":139,"line":1578},[137,19405,516],{"emptyLinePlaceholder":515},[137,19407,19408,19410,19412,19414,19416],{"class":139,"line":1588},[137,19409,14664],{"class":157},[137,19411,16319],{"class":147},[137,19413,356],{"class":157},[137,19415,16430],{"class":284},[137,19417,3155],{"class":157},[137,19419,19420,19422],{"class":139,"line":1601},[137,19421,16437],{"class":147},[137,19423,275],{"class":157},[137,19425,19426,19428,19430],{"class":139,"line":3802},[137,19427,5472],{"class":143},[137,19429,16446],{"class":284},[137,19431,3276],{"class":157},[137,19433,19434],{"class":139,"line":3808},[137,19435,294],{"class":157},[137,19437,19438],{"class":139,"line":3822},[137,19439,510],{"class":157},[27,19441,16459],{},[27,19443,19444,19445,1017],{},"In order to get the access token we will need to setup the Firebase client SDK. We will not go into the details of how to setup the client SDK at the moment. This will be a topic for a separate blog article. Firebase has great examples of that topic with all major frontend frameworks. You can find the documentation with examples in the following ",[45,19446,2726],{"href":19447,"target":2716,"rel":19448},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fbuild",[2718,2719],[104,19450,16470],{"id":16469},[27,19452,19453],{},"Let’s test out the protected endpoints with the access token obtained from the client app. We need to include the following in each request we make in the Authorisation header:",[128,19455,19456],{"className":15948,"code":16476,"language":15950,"meta":133,"style":133},[22,19457,19458,19462,19466,19470,19474],{"__ignoreMap":133},[137,19459,19460],{"class":139,"line":140},[137,19461,16483],{},[137,19463,19464],{"class":139,"line":173},[137,19465,15962],{},[137,19467,19468],{"class":139,"line":188},[137,19469,16492],{},[137,19471,19472],{"class":139,"line":269},[137,19473,516],{"emptyLinePlaceholder":515},[137,19475,19476],{"class":139,"line":278},[137,19477,16501],{},[27,19479,16504,19480,16509],{},[45,19481,16508],{"href":16507},[128,19483,19484],{"className":15948,"code":16512,"language":15950,"meta":133,"style":133},[22,19485,19486,19490],{"__ignoreMap":133},[137,19487,19488],{"class":139,"line":140},[137,19489,16519],{},[137,19491,19492],{"class":139,"line":173},[137,19493,16524],{},[27,19495,16527],{},[128,19497,19498],{"className":15948,"code":16530,"language":15950,"meta":133,"style":133},[22,19499,19500,19504,19508,19512,19516],{"__ignoreMap":133},[137,19501,19502],{"class":139,"line":140},[137,19503,16537],{},[137,19505,19506],{"class":139,"line":173},[137,19507,15971],{},[137,19509,19510],{"class":139,"line":188},[137,19511,16546],{},[137,19513,19514],{"class":139,"line":269},[137,19515,16551],{},[137,19517,19518],{"class":139,"line":278},[137,19519,510],{},[27,19521,19522],{},"And that’s all!",[27,19524,10126,19525,1017],{},[45,19526,2726],{"href":19527,"target":2716,"rel":19528},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fnest-firebase-auth",[2718,2719],[104,19530,2567],{"id":2566},[2569,19532,19533,19536,19542,19547],{},[1006,19534,19535],{},"This blog article shows how to sign up and sign in users with specific roles. Then giving those users access to only specific endpoints based on their roles. To accomplish that, we used Firebase auth to help us with the users Authentication and Authorisation.",[1006,19537,19538,19539,1017],{},"To create and validate users in our app we used the Firebase Admin package for Node.js SDK link ",[45,19540,2726],{"href":17250,"target":2716,"rel":19541},[2718,2719],[1006,19543,16594,19544,1017],{},[45,19545,16598],{"href":13794,"target":2716,"rel":19546},[2718,2719],[1006,19548,19549,19550,1017],{},"To be able to get the access token, we will need to setup the Firebase client SDK. Firebase has great documentation and examples on that topic with all the major frontend frameworks. You can find these examples in the following ",[45,19551,2726],{"href":19447,"target":2716,"rel":19552},[2718,2719],[2617,19554,16607],{},{"title":133,"searchDepth":173,"depth":173,"links":19556},[19557,19558,19559,19560,19561,19562,19563,19564,19565,19570,19571,19572,19573],{"id":12851,"depth":173,"text":12852},{"id":16691,"depth":173,"text":16692},{"id":12911,"depth":173,"text":12912},{"id":16883,"depth":173,"text":16884},{"id":13158,"depth":173,"text":13159},{"id":13254,"depth":173,"text":13255},{"id":17188,"depth":173,"text":17189},{"id":13784,"depth":173,"text":13785},{"id":17954,"depth":173,"text":17955,"children":19566},[19567,19568,19569],{"id":17961,"depth":188,"text":17962},{"id":14520,"depth":188,"text":14521},{"id":15127,"depth":188,"text":15128},{"id":15922,"depth":173,"text":15923},{"id":16066,"depth":173,"text":16067},{"id":16469,"depth":173,"text":16470},{"id":2566,"depth":173,"text":2567},"In this blog article we will be creating a Nest application where users (with different roles) can sign-up and sign-in to the application. Specific permissions can be configured for each user access to specific endpoints, based on the user role. We are going to use Firebase Auth to help us with user Authentication and Authorisation. Before we continue let’s first install the Nest CLI. That will help us create our project much quicker. To install Nest CLI globally use the following command in your terminal","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1664868269\u002Fblog\u002Fnestjs-authorisation-with-firebase-auth\u002Fnestjs-authorisation-with-firebase-auth",[16640,12816,12817,3537,16630,19577,5299,5300],"Firebase Auth",{},"\u002F2022\u002F10\u002F07\u002Fnestjs-authorisation-with-firebase-auth","7th Oct 2022",{"title":16640,"description":19574},"2022\u002F10\u002F07\u002Fnestjs-authorisation-with-firebase-auth","LLyLe4FZb6KioVx39lmJTJozadcH-sYl8koLMKomPHI",{"id":19585,"title":19586,"articleTags":19587,"author":11,"blog":12,"body":19588,"description":20858,"extension":2649,"image":20859,"keywords":20860,"meta":20862,"navigation":515,"path":20863,"published":20864,"readTime":278,"seo":20865,"stem":20866,"type":2662,"__hash__":20867},"content\u002F2022\u002F10\u002F10\u002Fnestjs-current-user-custom-decorator.md","Nest.js @CurrentUser Custom Decorator",[12816,2668,2669],{"type":14,"value":19589,"toc":20848},[19590,19593,19607,19609,19613,19618,19627,19634,19642,19648,19996,19999,20014,20017,20021,20033,20269,20272,20294,20301,20312,20458,20464,20480,20486,20507,20525,20552,20555,20590,20595,20614,20618,20781,20802,20804,20810,20812,20845],[17,19591,19586],{"id":19592},"nestjs-currentuser-custom-decorator",[27,19594,19595],{},[30,19596,19597,36,19599,40,19601],{},[33,19598],{"value":35},[33,19600],{"value":39},[42,19602,19603],{},[45,19604,19605],{"href":47},[33,19606],{"value":50},[52,19608],{":tags":54},[56,19610],{":audio-src":19611,":transcript-src":19612},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F10\u002F10\u002Fnestjs-current-user-custom-decorator\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F10\u002F10\u002Fnestjs-current-user-custom-decorator\u002Fsummary.json",[27,19614,19615],{},[63,19616],{"alt":12847,"src":19617},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1665138447\u002Fblog\u002Fnestjs-current-user-custom-decorator\u002Fnestjs-current-user-custom-decorator",[3244,19619,19620],{},[27,19621,19622,19623,1017],{},"This blog article is a continuation from the previous blog article about Nest.js Authorisation with Firebase Auth. If you have’t read that article you will not be able to follow along. The previous article can be found at the following ",[45,19624,2726],{"href":19625,"target":2716,"rel":19626},"https:\u002F\u002Fwww.trpkovski.com\u002F2022\u002F10\u002F07\u002Fnestjs-authorisation-with-firebase-auth\u002F",[2718,2719],[27,19628,19629,19630,19633],{},"In this article we will be using custom route decorators in Nest. We will be creating our own ",[22,19631,19632],{},"@CurrentUser"," decorator and use it in the module controllers anytime we want to get the current logged user. Let’s have a look at how we can achieve this.",[104,19635,19637,19638,19641],{"id":19636},"assign-claims-to-the-request-object-in-our-guard","Assign claims to the ",[22,19639,19640],{},"request"," object in our Guard.",[27,19643,19644,19645,19647],{},"First, make small changes to our ",[22,19646,13808],{}," file. Our auth guard code should appear as follows:",[128,19649,19651],{"className":13299,"code":19650,"language":13301,"meta":133,"style":133},"import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from \"@nestjs\u002Fcommon\";\nimport { Reflector } from \"@nestjs\u002Fcore\";\nimport { FirebaseAdmin } from \"..\u002F..\u002Fconfig\u002Ffirebase.setup\";\n\n@Injectable()\nexport class AuthGuard implements CanActivate {\n    constructor(\n        private reflector: Reflector,\n        private readonly admin: FirebaseAdmin\n    ) {}\n\n    async canActivate(context: ExecutionContext): Promise\u003Cboolean> {\n        const app = this.admin.setup();\n        const request = context.switchToHttp().getRequest();\n        const idToken = context.getArgs()[0]?.headers?.authorization.split(\" \")[1];\n\n        const permissions = this.reflector.get\u003Cstring[]>(\"permissions\", context.getHandler());\n        try {\n            const claims = await app.auth().verifyIdToken(idToken);\n\n            if (claims.role === permissions[0]) {\n                request.claims = claims;\n                return true;\n            }\n            throw new UnauthorizedException();\n        } catch (error) {\n            console.log(\"Error\", error);\n            throw new UnauthorizedException();\n        }\n    }\n}\n",[22,19652,19653,19665,19677,19689,19693,19701,19715,19721,19733,19745,19749,19753,19779,19795,19816,19846,19850,19878,19884,19904,19908,19922,19932,19940,19944,19954,19962,19974,19984,19988,19992],{"__ignoreMap":133},[137,19654,19655,19657,19659,19661,19663],{"class":139,"line":140},[137,19656,10287],{"class":143},[137,19658,13851],{"class":157},[137,19660,10954],{"class":143},[137,19662,13315],{"class":284},[137,19664,3276],{"class":157},[137,19666,19667,19669,19671,19673,19675],{"class":139,"line":173},[137,19668,10287],{"class":143},[137,19670,13912],{"class":157},[137,19672,10954],{"class":143},[137,19674,13917],{"class":284},[137,19676,3276],{"class":157},[137,19678,19679,19681,19683,19685,19687],{"class":139,"line":188},[137,19680,10287],{"class":143},[137,19682,17562],{"class":157},[137,19684,10954],{"class":143},[137,19686,17567],{"class":284},[137,19688,3276],{"class":157},[137,19690,19691],{"class":139,"line":269},[137,19692,516],{"emptyLinePlaceholder":515},[137,19694,19695,19697,19699],{"class":139,"line":278},[137,19696,13382],{"class":157},[137,19698,13604],{"class":147},[137,19700,2754],{"class":157},[137,19702,19703,19705,19707,19709,19711,19713],{"class":139,"line":291},[137,19704,13456],{"class":143},[137,19706,7832],{"class":143},[137,19708,17590],{"class":147},[137,19710,13943],{"class":143},[137,19712,13946],{"class":147},[137,19714,256],{"class":157},[137,19716,19717,19719],{"class":139,"line":297},[137,19718,3651],{"class":143},[137,19720,11813],{"class":157},[137,19722,19723,19725,19727,19729,19731],{"class":139,"line":302},[137,19724,13975],{"class":143},[137,19726,13978],{"class":161},[137,19728,894],{"class":143},[137,19730,13983],{"class":147},[137,19732,1961],{"class":157},[137,19734,19735,19737,19739,19741,19743],{"class":139,"line":662},[137,19736,13975],{"class":143},[137,19738,13992],{"class":143},[137,19740,17336],{"class":161},[137,19742,894],{"class":143},[137,19744,17627],{"class":147},[137,19746,19747],{"class":139,"line":667},[137,19748,14005],{"class":157},[137,19750,19751],{"class":139,"line":786},[137,19752,516],{"emptyLinePlaceholder":515},[137,19754,19755,19757,19759,19761,19763,19765,19767,19769,19771,19773,19775,19777],{"class":139,"line":798},[137,19756,15546],{"class":143},[137,19758,17642],{"class":147},[137,19760,356],{"class":157},[137,19762,14097],{"class":161},[137,19764,894],{"class":143},[137,19766,14102],{"class":147},[137,19768,14105],{"class":157},[137,19770,894],{"class":143},[137,19772,14116],{"class":147},[137,19774,4033],{"class":157},[137,19776,14121],{"class":364},[137,19778,14136],{"class":157},[137,19780,19781,19783,19785,19787,19789,19791,19793],{"class":139,"line":803},[137,19782,3008],{"class":143},[137,19784,15064],{"class":364},[137,19786,151],{"class":143},[137,19788,365],{"class":364},[137,19790,17675],{"class":157},[137,19792,15609],{"class":147},[137,19794,924],{"class":157},[137,19796,19797,19799,19802,19804,19806,19809,19811,19814],{"class":139,"line":931},[137,19798,3008],{"class":143},[137,19800,19801],{"class":364}," request",[137,19803,151],{"class":143},[137,19805,14204],{"class":157},[137,19807,19808],{"class":147},"switchToHttp",[137,19810,17766],{"class":157},[137,19812,19813],{"class":147},"getRequest",[137,19815,924],{"class":157},[137,19817,19818,19820,19822,19824,19826,19828,19830,19832,19834,19836,19838,19840,19842,19844],{"class":139,"line":1568},[137,19819,3008],{"class":143},[137,19821,17686],{"class":364},[137,19823,151],{"class":143},[137,19825,14204],{"class":157},[137,19827,14207],{"class":147},[137,19829,14210],{"class":157},[137,19831,6044],{"class":364},[137,19833,14215],{"class":157},[137,19835,8537],{"class":147},[137,19837,356],{"class":157},[137,19839,8542],{"class":284},[137,19841,14224],{"class":157},[137,19843,6065],{"class":364},[137,19845,5727],{"class":157},[137,19847,19848],{"class":139,"line":1573},[137,19849,516],{"emptyLinePlaceholder":515},[137,19851,19852,19854,19856,19858,19860,19862,19864,19866,19868,19870,19872,19874,19876],{"class":139,"line":1578},[137,19853,3008],{"class":143},[137,19855,14143],{"class":364},[137,19857,151],{"class":143},[137,19859,365],{"class":364},[137,19861,14150],{"class":157},[137,19863,14153],{"class":147},[137,19865,4033],{"class":157},[137,19867,14158],{"class":364},[137,19869,14161],{"class":157},[137,19871,14164],{"class":284},[137,19873,14167],{"class":157},[137,19875,14170],{"class":147},[137,19877,14173],{"class":157},[137,19879,19880,19882],{"class":139,"line":1588},[137,19881,15697],{"class":143},[137,19883,256],{"class":157},[137,19885,19886,19888,19890,19892,19894,19896,19898,19900,19902],{"class":139,"line":1601},[137,19887,5772],{"class":143},[137,19889,17755],{"class":364},[137,19891,151],{"class":143},[137,19893,15069],{"class":143},[137,19895,15103],{"class":157},[137,19897,2751],{"class":147},[137,19899,17766],{"class":157},[137,19901,2983],{"class":147},[137,19903,17771],{"class":157},[137,19905,19906],{"class":139,"line":3802},[137,19907,516],{"emptyLinePlaceholder":515},[137,19909,19910,19912,19914,19916,19918,19920],{"class":139,"line":3808},[137,19911,5747],{"class":143},[137,19913,17782],{"class":157},[137,19915,5502],{"class":143},[137,19917,17787],{"class":157},[137,19919,6044],{"class":364},[137,19921,17792],{"class":157},[137,19923,19924,19927,19929],{"class":139,"line":3822},[137,19925,19926],{"class":157},"                request.claims ",[137,19928,253],{"class":143},[137,19930,19931],{"class":157}," claims;\n",[137,19933,19934,19936,19938],{"class":139,"line":3827},[137,19935,5761],{"class":143},[137,19937,14286],{"class":364},[137,19939,3276],{"class":157},[137,19941,19942],{"class":139,"line":3832},[137,19943,760],{"class":157},[137,19945,19946,19948,19950,19952],{"class":139,"line":3840},[137,19947,15739],{"class":143},[137,19949,1426],{"class":143},[137,19951,14329],{"class":147},[137,19953,924],{"class":157},[137,19955,19956,19958,19960],{"class":139,"line":3846},[137,19957,15729],{"class":157},[137,19959,2807],{"class":143},[137,19961,15734],{"class":157},[137,19963,19964,19966,19968,19970,19972],{"class":139,"line":3861},[137,19965,1493],{"class":157},[137,19967,353],{"class":147},[137,19969,356],{"class":157},[137,19971,17833],{"class":284},[137,19973,17836],{"class":157},[137,19975,19976,19978,19980,19982],{"class":139,"line":3883},[137,19977,15739],{"class":143},[137,19979,1426],{"class":143},[137,19981,14329],{"class":147},[137,19983,924],{"class":157},[137,19985,19986],{"class":139,"line":3896},[137,19987,1966],{"class":157},[137,19989,19990],{"class":139,"line":3901},[137,19991,294],{"class":157},[137,19993,19994],{"class":139,"line":3906},[137,19995,510],{"class":157},[27,19997,19998],{},"The two changes made to the existing file are:",[1003,20000,20001,20008],{},[1006,20002,20003,20004,20007],{},"Getting the request object: ",[22,20005,20006],{},"const request = context.switchToHttp().getRequest()",";",[1006,20009,20010,20011],{},"Assigning the user claims in a new property in the request object: ",[22,20012,20013],{},"request.claims = claims;",[27,20015,20016],{},"We will next create an interceptor that will help us to get the current user from Firebase.",[104,20018,20020],{"id":20019},"create-an-interceptor","Create an Interceptor",[27,20022,20023,20024,13801,20026,20029,20030,13575],{},"Create a new directory in ",[22,20025,13088],{},[22,20027,20028],{},"interceptors\u002F"," . Inside the directory, create a new file called ",[22,20031,20032],{},"current-user.interceptor.ts",[128,20034,20036],{"className":13299,"code":20035,"language":13301,"meta":133,"style":133},"import { Injectable, NestInterceptor, ExecutionContext, CallHandler, BadRequestException } from \"@nestjs\u002Fcommon\";\nimport { FirebaseAdmin } from \"..\u002F..\u002Fconfig\u002Ffirebase.setup\";\n\n@Injectable()\nexport class CurrentUserInterceptor implements NestInterceptor {\n    constructor(private readonly admin: FirebaseAdmin) {}\n\n    async intercept(context: ExecutionContext, handler: CallHandler) {\n        const app = this.admin.setup();\n        const request = context.switchToHttp().getRequest();\n\n        try {\n            const user = await app.auth().getUser(request.claims.uid);\n            request.currentUser = user;\n        } catch (error) {\n            console.log(\"Error\", error);\n            throw new BadRequestException();\n        }\n        return handler.handle();\n    }\n}\n",[22,20037,20038,20051,20063,20067,20075,20091,20109,20113,20140,20156,20174,20178,20184,20206,20215,20223,20235,20245,20249,20261,20265],{"__ignoreMap":133},[137,20039,20040,20042,20045,20047,20049],{"class":139,"line":140},[137,20041,10287],{"class":143},[137,20043,20044],{"class":157}," { Injectable, NestInterceptor, ExecutionContext, CallHandler, BadRequestException } ",[137,20046,10954],{"class":143},[137,20048,13315],{"class":284},[137,20050,3276],{"class":157},[137,20052,20053,20055,20057,20059,20061],{"class":139,"line":173},[137,20054,10287],{"class":143},[137,20056,17562],{"class":157},[137,20058,10954],{"class":143},[137,20060,17567],{"class":284},[137,20062,3276],{"class":157},[137,20064,20065],{"class":139,"line":188},[137,20066,516],{"emptyLinePlaceholder":515},[137,20068,20069,20071,20073],{"class":139,"line":269},[137,20070,13382],{"class":157},[137,20072,13604],{"class":147},[137,20074,2754],{"class":157},[137,20076,20077,20079,20081,20084,20086,20089],{"class":139,"line":278},[137,20078,13456],{"class":143},[137,20080,7832],{"class":143},[137,20082,20083],{"class":147}," CurrentUserInterceptor",[137,20085,13943],{"class":143},[137,20087,20088],{"class":147}," NestInterceptor",[137,20090,256],{"class":157},[137,20092,20093,20095,20097,20099,20101,20103,20105,20107],{"class":139,"line":291},[137,20094,3651],{"class":143},[137,20096,356],{"class":157},[137,20098,15234],{"class":143},[137,20100,13992],{"class":143},[137,20102,17336],{"class":161},[137,20104,894],{"class":143},[137,20106,17368],{"class":147},[137,20108,15246],{"class":157},[137,20110,20111],{"class":139,"line":297},[137,20112,516],{"emptyLinePlaceholder":515},[137,20114,20115,20117,20120,20122,20124,20126,20128,20130,20133,20135,20138],{"class":139,"line":302},[137,20116,15546],{"class":143},[137,20118,20119],{"class":147}," intercept",[137,20121,356],{"class":157},[137,20123,14097],{"class":161},[137,20125,894],{"class":143},[137,20127,14102],{"class":147},[137,20129,164],{"class":157},[137,20131,20132],{"class":161},"handler",[137,20134,894],{"class":143},[137,20136,20137],{"class":147}," CallHandler",[137,20139,170],{"class":157},[137,20141,20142,20144,20146,20148,20150,20152,20154],{"class":139,"line":662},[137,20143,3008],{"class":143},[137,20145,15064],{"class":364},[137,20147,151],{"class":143},[137,20149,365],{"class":364},[137,20151,17675],{"class":157},[137,20153,15609],{"class":147},[137,20155,924],{"class":157},[137,20157,20158,20160,20162,20164,20166,20168,20170,20172],{"class":139,"line":667},[137,20159,3008],{"class":143},[137,20161,19801],{"class":364},[137,20163,151],{"class":143},[137,20165,14204],{"class":157},[137,20167,19808],{"class":147},[137,20169,17766],{"class":157},[137,20171,19813],{"class":147},[137,20173,924],{"class":157},[137,20175,20176],{"class":139,"line":786},[137,20177,516],{"emptyLinePlaceholder":515},[137,20179,20180,20182],{"class":139,"line":798},[137,20181,15697],{"class":143},[137,20183,256],{"class":157},[137,20185,20186,20188,20190,20192,20194,20196,20198,20200,20203],{"class":139,"line":803},[137,20187,5772],{"class":143},[137,20189,13217],{"class":364},[137,20191,151],{"class":143},[137,20193,15069],{"class":143},[137,20195,15103],{"class":157},[137,20197,2751],{"class":147},[137,20199,17766],{"class":157},[137,20201,20202],{"class":147},"getUser",[137,20204,20205],{"class":157},"(request.claims.uid);\n",[137,20207,20208,20211,20213],{"class":139,"line":931},[137,20209,20210],{"class":157},"            request.currentUser ",[137,20212,253],{"class":143},[137,20214,15724],{"class":157},[137,20216,20217,20219,20221],{"class":139,"line":1568},[137,20218,15729],{"class":157},[137,20220,2807],{"class":143},[137,20222,15734],{"class":157},[137,20224,20225,20227,20229,20231,20233],{"class":139,"line":1573},[137,20226,1493],{"class":157},[137,20228,353],{"class":147},[137,20230,356],{"class":157},[137,20232,17833],{"class":284},[137,20234,17836],{"class":157},[137,20236,20237,20239,20241,20243],{"class":139,"line":1578},[137,20238,15739],{"class":143},[137,20240,1426],{"class":143},[137,20242,15744],{"class":147},[137,20244,924],{"class":157},[137,20246,20247],{"class":139,"line":1588},[137,20248,1966],{"class":157},[137,20250,20251,20253,20256,20259],{"class":139,"line":1601},[137,20252,5472],{"class":143},[137,20254,20255],{"class":157}," handler.",[137,20257,20258],{"class":147},"handle",[137,20260,924],{"class":157},[137,20262,20263],{"class":139,"line":3802},[137,20264,294],{"class":157},[137,20266,20267],{"class":139,"line":3808},[137,20268,510],{"class":157},[27,20270,20271],{},"Let me explain what is going on in the code above.",[27,20273,20274,20275,2107,20278,20280,20281,20283,20284,20287,20288,20290,20291,1017],{},"From the ",[22,20276,20277],{},"claims",[22,20279,19640],{}," object we can obtain the user’s ",[22,20282,3438],{},". Calling ",[22,20285,20286],{},"getUser()"," firebase auth function with the ",[22,20289,3438],{},", we can obtain the user’s details. Lastly, we assigned that user in the request object ",[22,20292,20293],{},"request.currentUser = user",[104,20295,20297,20298,20300],{"id":20296},"create-currentuser-decorator","Create ",[22,20299,19632],{}," Decorator",[27,20302,20303,20304,20307,20308,20311],{},"Create a new file in ",[22,20305,20306],{},"src\u002Fdecorators\u002F"," directory called ",[22,20309,20310],{},"current-user.decorators.ts",". Inside this directory, place the following code:",[128,20313,20315],{"className":13299,"code":20314,"language":13301,"meta":133,"style":133},"import { createParamDecorator, ExecutionContext } from \"@nestjs\u002Fcommon\";\nimport * as adminTypes from \"firebase-admin\";\n\ntype UserRecord = keyof adminTypes.auth.UserRecord;\n\nexport const CurrentUser = createParamDecorator((data: UserRecord, context: ExecutionContext) => {\n    const request = context.switchToHttp().getRequest();\n    return data ? request.currentUser?.[data] : request.currentUser;\n});\n",[22,20316,20317,20330,20347,20351,20378,20382,20419,20437,20454],{"__ignoreMap":133},[137,20318,20319,20321,20324,20326,20328],{"class":139,"line":140},[137,20320,10287],{"class":143},[137,20322,20323],{"class":157}," { createParamDecorator, ExecutionContext } ",[137,20325,10954],{"class":143},[137,20327,13315],{"class":284},[137,20329,3276],{"class":157},[137,20331,20332,20334,20336,20338,20341,20343,20345],{"class":139,"line":173},[137,20333,10287],{"class":143},[137,20335,13878],{"class":364},[137,20337,13881],{"class":143},[137,20339,20340],{"class":157}," adminTypes ",[137,20342,10954],{"class":143},[137,20344,17323],{"class":284},[137,20346,3276],{"class":157},[137,20348,20349],{"class":139,"line":188},[137,20350,516],{"emptyLinePlaceholder":515},[137,20352,20353,20356,20359,20361,20364,20367,20369,20371,20373,20376],{"class":139,"line":269},[137,20354,20355],{"class":143},"type",[137,20357,20358],{"class":147}," UserRecord",[137,20360,151],{"class":143},[137,20362,20363],{"class":143}," keyof",[137,20365,20366],{"class":147}," adminTypes",[137,20368,1017],{"class":157},[137,20370,2751],{"class":147},[137,20372,1017],{"class":157},[137,20374,20375],{"class":147},"UserRecord",[137,20377,3276],{"class":157},[137,20379,20380],{"class":139,"line":278},[137,20381,516],{"emptyLinePlaceholder":515},[137,20383,20384,20386,20389,20392,20394,20397,20399,20401,20403,20405,20407,20409,20411,20413,20415,20417],{"class":139,"line":291},[137,20385,13456],{"class":143},[137,20387,20388],{"class":143}," const",[137,20390,20391],{"class":364}," CurrentUser",[137,20393,151],{"class":143},[137,20395,20396],{"class":147}," createParamDecorator",[137,20398,2774],{"class":157},[137,20400,1942],{"class":161},[137,20402,894],{"class":143},[137,20404,20358],{"class":147},[137,20406,164],{"class":157},[137,20408,14097],{"class":161},[137,20410,894],{"class":143},[137,20412,14102],{"class":147},[137,20414,219],{"class":157},[137,20416,222],{"class":143},[137,20418,256],{"class":157},[137,20420,20421,20423,20425,20427,20429,20431,20433,20435],{"class":139,"line":297},[137,20422,4177],{"class":143},[137,20424,19801],{"class":364},[137,20426,151],{"class":143},[137,20428,14204],{"class":157},[137,20430,19808],{"class":147},[137,20432,17766],{"class":157},[137,20434,19813],{"class":147},[137,20436,924],{"class":157},[137,20438,20439,20441,20444,20446,20449,20451],{"class":139,"line":302},[137,20440,176],{"class":143},[137,20442,20443],{"class":157}," data ",[137,20445,12972],{"class":143},[137,20447,20448],{"class":157}," request.currentUser?.[data] ",[137,20450,894],{"class":143},[137,20452,20453],{"class":157}," request.currentUser;\n",[137,20455,20456],{"class":139,"line":662},[137,20457,5422],{"class":157},[27,20459,20460,20461,20463],{},"In our custom decorator, we’ve returned the current user from the ",[22,20462,19640],{}," object.",[27,20465,20466,20467,20469,20470,20473,20474,20476,20477,20479],{},"We can also pass data to our custom decorator by using the ",[22,20468,1942],{}," parameter in the ",[22,20471,20472],{},"createParamDecorator"," function. In our case, we can pass any user property. If a user property is passed then only that property will be returned. If there is nothing passed then the whole user object is returned. To make sure that only user property is passed in the ",[22,20475,1942],{}," parameter, we create a ",[22,20478,20355],{}," that can be any property of the user object. We can accomplish this via the following:",[27,20481,20482,20483],{},"First, import ",[22,20484,20485],{},"firebase-admin",[128,20487,20489],{"className":13299,"code":20488,"language":13301,"meta":133,"style":133},"import * as adminTypes from \"firebase-admin\";\n",[22,20490,20491],{"__ignoreMap":133},[137,20492,20493,20495,20497,20499,20501,20503,20505],{"class":139,"line":140},[137,20494,10287],{"class":143},[137,20496,13878],{"class":364},[137,20498,13881],{"class":143},[137,20500,20340],{"class":157},[137,20502,10954],{"class":143},[137,20504,17323],{"class":284},[137,20506,3276],{"class":157},[27,20508,20509,20510,13801,20512,20514,20515,20518,20519,20521,20522,1017],{},"Then create a ",[22,20511,20355],{},[22,20513,20375],{}," from ",[22,20516,20517],{},"auth.UserRecord"," interface. Get all user properties and assign them to our type ",[22,20520,20375],{}," by using the TypeScript built in method called ",[22,20523,20524],{},"keyof",[128,20526,20528],{"className":13299,"code":20527,"language":13301,"meta":133,"style":133},"type UserRecord = keyof adminTypes.auth.UserRecord;\n",[22,20529,20530],{"__ignoreMap":133},[137,20531,20532,20534,20536,20538,20540,20542,20544,20546,20548,20550],{"class":139,"line":140},[137,20533,20355],{"class":143},[137,20535,20358],{"class":147},[137,20537,151],{"class":143},[137,20539,20363],{"class":143},[137,20541,20366],{"class":147},[137,20543,1017],{"class":157},[137,20545,2751],{"class":147},[137,20547,1017],{"class":157},[137,20549,20375],{"class":147},[137,20551,3276],{"class":157},[27,20553,20554],{},"Lastly, assign that type to the data Object:",[128,20556,20558],{"className":13299,"code":20557,"language":13301,"meta":133,"style":133},"async (data: UserRecord, context: ExecutionContext) => {\n...\n",[22,20559,20560,20586],{"__ignoreMap":133},[137,20561,20562,20564,20566,20568,20570,20572,20574,20576,20578,20580,20582,20584],{"class":139,"line":140},[137,20563,15050],{"class":143},[137,20565,158],{"class":157},[137,20567,1942],{"class":161},[137,20569,894],{"class":143},[137,20571,20358],{"class":147},[137,20573,164],{"class":157},[137,20575,14097],{"class":161},[137,20577,894],{"class":143},[137,20579,14102],{"class":147},[137,20581,219],{"class":157},[137,20583,222],{"class":143},[137,20585,256],{"class":157},[137,20587,20588],{"class":139,"line":173},[137,20589,4058],{"class":143},[104,20591,9726,20593,20300],{"id":20592},"use-currentuser-decorator",[22,20594,19632],{},[27,20596,20597,20598,20600,20601,114,20604,20607,20608,15443,20610,20613],{},"To be able to use our ",[22,20599,19632],{}," decorator in our application, we will first need to import both the ",[22,20602,20603],{},"CurrentUserInterceptor",[22,20605,20606],{},"CurrentUser"," decorator inside our controller, and then call ",[22,20609,20603],{},[22,20611,20612],{},"@UseInterceptors"," decorator.",[27,20615,16255,20616,15451],{},[22,20617,13099],{},[128,20619,20621],{"className":13299,"code":20620,"language":13301,"meta":133,"style":133},"import { Controller, Get, UseInterceptors } from '@nestjs\u002Fcommon';\nimport { Auth } from '.\u002Fdecorators\u002Fauth.decorator';\nimport { CurrentUser } from '.\u002Fdecorators\u002Fcurrent-user.decorators';\nimport { CurrentUserInterceptor } from '.\u002Finterceptors\u002Fcurrent-user.interceptor';\n\n@Controller()\n@UseInterceptors(CurrentUserInterceptor)\nexport class AppController {\n  @Get('\u002Fmorning')\n  @Auth('ADMIN')\n  goodMorning(@CurrentUser('email') email: string) {\n    return 'Good Morning!' + email;\n  }\n...\n",[22,20622,20623,20637,20650,20664,20678,20682,20690,20700,20710,20724,20737,20761,20773,20777],{"__ignoreMap":133},[137,20624,20625,20627,20630,20632,20635],{"class":139,"line":140},[137,20626,10287],{"class":143},[137,20628,20629],{"class":157}," { Controller, Get, UseInterceptors } ",[137,20631,10954],{"class":143},[137,20633,20634],{"class":284}," '@nestjs\u002Fcommon'",[137,20636,3276],{"class":157},[137,20638,20639,20641,20643,20645,20648],{"class":139,"line":173},[137,20640,10287],{"class":143},[137,20642,16282],{"class":157},[137,20644,10954],{"class":143},[137,20646,20647],{"class":284}," '.\u002Fdecorators\u002Fauth.decorator'",[137,20649,3276],{"class":157},[137,20651,20652,20654,20657,20659,20662],{"class":139,"line":188},[137,20653,10287],{"class":143},[137,20655,20656],{"class":157}," { CurrentUser } ",[137,20658,10954],{"class":143},[137,20660,20661],{"class":284}," '.\u002Fdecorators\u002Fcurrent-user.decorators'",[137,20663,3276],{"class":157},[137,20665,20666,20668,20671,20673,20676],{"class":139,"line":269},[137,20667,10287],{"class":143},[137,20669,20670],{"class":157}," { CurrentUserInterceptor } ",[137,20672,10954],{"class":143},[137,20674,20675],{"class":284}," '.\u002Finterceptors\u002Fcurrent-user.interceptor'",[137,20677,3276],{"class":157},[137,20679,20680],{"class":139,"line":278},[137,20681,516],{"emptyLinePlaceholder":515},[137,20683,20684,20686,20688],{"class":139,"line":291},[137,20685,13382],{"class":157},[137,20687,15818],{"class":147},[137,20689,2754],{"class":157},[137,20691,20692,20694,20697],{"class":139,"line":297},[137,20693,13382],{"class":157},[137,20695,20696],{"class":147},"UseInterceptors",[137,20698,20699],{"class":157},"(CurrentUserInterceptor)\n",[137,20701,20702,20704,20706,20708],{"class":139,"line":302},[137,20703,13456],{"class":143},[137,20705,7832],{"class":143},[137,20707,16310],{"class":147},[137,20709,256],{"class":157},[137,20711,20712,20715,20717,20719,20722],{"class":139,"line":662},[137,20713,20714],{"class":157},"  @",[137,20716,16319],{"class":147},[137,20718,356],{"class":157},[137,20720,20721],{"class":284},"'\u002Fmorning'",[137,20723,3155],{"class":157},[137,20725,20726,20728,20730,20732,20735],{"class":139,"line":667},[137,20727,20714],{"class":157},[137,20729,16333],{"class":147},[137,20731,356],{"class":157},[137,20733,20734],{"class":284},"'ADMIN'",[137,20736,3155],{"class":157},[137,20738,20739,20742,20744,20746,20748,20751,20753,20755,20757,20759],{"class":139,"line":786},[137,20740,20741],{"class":147},"  goodMorning",[137,20743,15881],{"class":157},[137,20745,20606],{"class":147},[137,20747,356],{"class":157},[137,20749,20750],{"class":284},"'email'",[137,20752,219],{"class":157},[137,20754,15569],{"class":161},[137,20756,894],{"class":143},[137,20758,13630],{"class":364},[137,20760,170],{"class":157},[137,20762,20763,20765,20768,20770],{"class":139,"line":798},[137,20764,176],{"class":143},[137,20766,20767],{"class":284}," 'Good Morning!'",[137,20769,361],{"class":143},[137,20771,20772],{"class":157}," email;\n",[137,20774,20775],{"class":139,"line":803},[137,20776,3462],{"class":157},[137,20778,20779],{"class":139,"line":931},[137,20780,4058],{"class":143},[27,20782,20783,20784,20786,20787,20789,20790,164,20792,164,20795,20798,20799,20801],{},"In the example above, our ",[22,20785,19632],{}," decorator will return the user’s ",[22,20788,15569],{}," only. We can pass any user’s property we want. For example, we can pass ",[22,20791,3438],{},[22,20793,20794],{},"displayName",[22,20796,20797],{},"phoneNumber"," etc. If we don’t pass anything, the ",[22,20800,19632],{}," will return the whole user object.",[27,20803,19522],{},[27,20805,10126,20806,1017],{},[45,20807,2726],{"href":20808,"target":2716,"rel":20809},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fnest-current-user-custom-decorator",[2718,2719],[104,20811,2567],{"id":2566},[2569,20813,20814,20820,20823,20839],{},[1006,20815,20816,20817,20819],{},"In this article, we created a custom decorator called ",[22,20818,19632],{}," that will help us to get the user in the module controller.",[1006,20821,20822],{},"To be able to get the current user, we made use of a custom interceptor in Nest.",[1006,20824,20825,20826,13801,20828,20830,20831,20833,20834,20836,20837,1017],{},"In our custom decorator we can also pass data. To be able to verify the data that is passed, we created a ",[22,20827,20355],{},[22,20829,20375],{}," from the ",[22,20832,20517],{}," interface. With the TypeScript built in method called ",[22,20835,20524],{},", we were able to extract all the properties from the ",[22,20838,20517],{},[1006,20840,20841,20842,1017],{},"This article was continuation from the previous article about Nest.js Authorisation with Firebase Auth. If you have’t read that article, you can find the article in the following ",[45,20843,2726],{"href":19625,"target":2716,"rel":20844},[2718,2719],[2617,20846,20847],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":133,"searchDepth":173,"depth":173,"links":20849},[20850,20852,20853,20855,20857],{"id":19636,"depth":173,"text":20851},"Assign claims to the request object in our Guard.",{"id":20019,"depth":173,"text":20020},{"id":20296,"depth":173,"text":20854},"Create @CurrentUser Decorator",{"id":20592,"depth":173,"text":20856},"Use @CurrentUser Decorator",{"id":2566,"depth":173,"text":2567},"This blog article is a continuation from the previous blog article about Nest.js Authorisation with Firebase Auth. If you have’t read that article you will not be able to follow along. The previous article can be found below. In this article we will be using custom route decorators in Nest. We will be creating our own @CurrentUser decorator and use it in the module controllers anytime we want to get the current logged user. Let’s have a look at how we can achieve this.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1665138447\u002Fblog\u002Fnestjs-current-user-custom-decorator\u002Fnestjs-current-user-custom-decorator",[19586,12816,12817,20861,16333,19577,5299,5300],"Logged User",{},"\u002F2022\u002F10\u002F10\u002Fnestjs-current-user-custom-decorator","10th Oct 2022",{"title":19586,"description":20858},"2022\u002F10\u002F10\u002Fnestjs-current-user-custom-decorator","RuJcL2_iW52dXHLrH7vTXE_FL2vj8Bzu7lDk3wjRbVg",{"id":20869,"title":20870,"articleTags":20871,"author":11,"blog":12,"body":20872,"description":21080,"extension":2649,"image":21081,"keywords":21082,"meta":21086,"navigation":515,"path":21087,"published":21088,"readTime":140,"seo":21089,"stem":21090,"type":2662,"__hash__":21091},"content\u002F2022\u002F10\u002F14\u002Fnestjs-class-validator-high-vulnerability-fix.md","Nest.js class-validator high vulnerability fix",[2669,12816,12817],{"type":14,"value":20873,"toc":21076},[20874,20881,20895,20897,20901,20906,20916,20931,20943,20949,20959,20976,20981,20998,21005,21065,21068,21070,21073],[17,20875,20877,20878,20880],{"id":20876},"nestjs-class-validator-high-vulnerability-fix","Nest.js ",[22,20879,14527],{}," high vulnerability fix",[27,20882,20883],{},[30,20884,20885,36,20887,40,20889],{},[33,20886],{"value":35},[33,20888],{"value":39},[42,20890,20891],{},[45,20892,20893],{"href":47},[33,20894],{"value":50},[52,20896],{":tags":54},[56,20898],{":audio-src":20899,":transcript-src":20900},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F10\u002F14\u002Fnestjs-class-validator-high-vulnerability-fix\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F10\u002F14\u002Fnestjs-class-validator-high-vulnerability-fix\u002Fsummary.json",[27,20902,20903],{},[63,20904],{"alt":12847,"src":20905},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1665729617\u002Fblog\u002Fnestjs-class-validator-high-vulnerability-fix\u002Fnestjs-class-validator-high-vulnerability-fix",[27,20907,20908,20909,20915],{},"If you have used Nest.js recently, you probably have realised that the ",[45,20910,20913],{"href":20911,"target":2716,"rel":20912},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fclass-validator",[2718,2719],[22,20914,14527],{}," library has a high vulnerability in it, which has not been addressed for quite a while.",[27,20917,4737,20918,20924,20925,20927,20928,20930],{},[45,20919,20922],{"href":20920,"target":2716,"rel":20921},"https:\u002F\u002Fdocs.nestjs.com\u002Ftechniques\u002Fvalidation",[2718,2719],[22,20923,14990],{}," uses the powerful ",[22,20926,14527],{}," package and its declarative validation decorators. The ",[22,20929,14990],{}," provides a convenient approach to enforce validation rules for all incoming client payloads. The specific rules are declared with simple annotations in each module's local class\u002FDTO declarations.",[27,20932,4737,20933,20935,20936,20942],{},[22,20934,14527],{}," package works in conjunction with another package ",[45,20937,20940],{"href":20938,"target":2716,"rel":20939},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fclass-transformer",[2718,2719],[22,20941,14531],{},". The lack of maintenance made the Nuxt team fork the original packages and took care of the maintenance.",[104,20944,20946],{"id":20945},"how-to-migrate-to-the-new-forked-packages",[42,20947,20948],{},"How to migrate to the new forked packages",[2569,20950,20951],{},[1006,20952,20953,20954,114,20956,20958],{},"Uninstall the existing ",[22,20955,14527],{},[22,20957,14531],{}," packages from the Nest project.",[128,20960,20962],{"className":8665,"code":20961,"language":8667,"meta":133,"style":133},"npm uninstall class-validator class-transformer\n",[22,20963,20964],{"__ignoreMap":133},[137,20965,20966,20968,20971,20973],{"class":139,"line":140},[137,20967,9536],{"class":147},[137,20969,20970],{"class":284}," uninstall",[137,20972,14545],{"class":284},[137,20974,20975],{"class":284}," class-transformer\n",[2569,20977,20978],{},[1006,20979,20980],{},"Install the newly forked packages.",[128,20982,20984],{"className":8665,"code":20983,"language":8667,"meta":133,"style":133},"npm install @nestjs\u002Fclass-validator @nestjs\u002Fclass-transformer\n",[22,20985,20986],{"__ignoreMap":133},[137,20987,20988,20990,20992,20995],{"class":139,"line":140},[137,20989,9536],{"class":147},[137,20991,10268],{"class":284},[137,20993,20994],{"class":284}," @nestjs\u002Fclass-validator",[137,20996,20997],{"class":284}," @nestjs\u002Fclass-transformer\n",[2569,20999,21000],{},[1006,21001,4286,21002,21004],{},[22,21003,13114],{}," file, add the following:",[128,21006,21008],{"className":13299,"code":21007,"language":13301,"meta":133,"style":133},"app.useGlobalPipes(\n    new ValidationPipe({\n        validatorPackage: require(\"@nestjs\u002Fclass-validator\"),\n        transformerPackage: require(\"@nestjs\u002Fclass-transformer\"),\n    })\n);\n",[22,21009,21010,21019,21028,21043,21057,21061],{"__ignoreMap":133},[137,21011,21012,21015,21017],{"class":139,"line":140},[137,21013,21014],{"class":157},"app.",[137,21016,15086],{"class":147},[137,21018,11813],{"class":157},[137,21020,21021,21024,21026],{"class":139,"line":173},[137,21022,21023],{"class":143},"    new",[137,21025,15093],{"class":147},[137,21027,3175],{"class":157},[137,21029,21030,21033,21036,21038,21041],{"class":139,"line":188},[137,21031,21032],{"class":157},"        validatorPackage: ",[137,21034,21035],{"class":147},"require",[137,21037,356],{"class":157},[137,21039,21040],{"class":284},"\"@nestjs\u002Fclass-validator\"",[137,21042,4517],{"class":157},[137,21044,21045,21048,21050,21052,21055],{"class":139,"line":269},[137,21046,21047],{"class":157},"        transformerPackage: ",[137,21049,21035],{"class":147},[137,21051,356],{"class":157},[137,21053,21054],{"class":284},"\"@nestjs\u002Fclass-transformer\"",[137,21056,4517],{"class":157},[137,21058,21059],{"class":139,"line":278},[137,21060,2800],{"class":157},[137,21062,21063],{"class":139,"line":291},[137,21064,1502],{"class":157},[27,21066,21067],{},"And that's all! Now we can start using the newly maintained packages.",[104,21069,2567],{"id":2566},[27,21071,21072],{},"This solution is a workaround for the time being until the Nest team figures out what is the best approach to migrate in the future.",[2617,21074,21075],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":133,"searchDepth":173,"depth":173,"links":21077},[21078,21079],{"id":20945,"depth":173,"text":20948},{"id":2566,"depth":173,"text":2567},"If you have used Nest.js recently probably have realised that the class-validator library has a high vulnerability in it, which is not being addressed for quite a while. The ValidationPipe makes use of the powerful class-validator package and its declarative validation decorators. The ValidationPipe provides a convenient approach to enforce validation rules for all incoming client payloads, where the specific rules are declared with simple annotations in local class\u002FDTO declarations in each module. The class-validator package works in conjunction with another package class-transformer. The lack of maintenance made the Nuxt team fork the original packages and take care of the maintenance.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1665729617\u002Fblog\u002Fnestjs-class-validator-high-vulnerability-fix\u002Fnestjs-class-validator-high-vulnerability-fix",[20870,12816,12817,21083,21084,21085,5299,5300],"Vulnerability","Fix","NPM",{},"\u002F2022\u002F10\u002F14\u002Fnestjs-class-validator-high-vulnerability-fix","14th Oct 2022",{"title":20870,"description":21080},"2022\u002F10\u002F14\u002Fnestjs-class-validator-high-vulnerability-fix","TEjGXBBAiCBpmbj_iEnD7TaGRJ3PHcs5aEKK-pkDZCU",{"id":21093,"title":21094,"articleTags":21095,"author":11,"blog":12,"body":21097,"description":22208,"extension":2649,"image":22209,"keywords":22210,"meta":22213,"navigation":515,"path":22214,"published":22215,"readTime":278,"seo":22216,"stem":22217,"type":2662,"__hash__":22218},"content\u002F2022\u002F10\u002F22\u002Fsharing-components-between-multiple-nuxt-projects.md","Sharing components between multiple Nuxt projects",[21096,8,12817],"Nuxt.js",{"type":14,"value":21098,"toc":22200},[21099,21102,21116,21118,21122,21127,21136,21145,21148,21151,21155,21158,21176,21183,21200,21206,21210,21213,21241,21244,21299,21308,21314,21550,21556,21675,21682,21696,21702,21712,21750,21761,21785,21797,21811,21816,21871,21882,21921,21924,21937,21942,21946,21953,22063,22073,22147,22153,22168,22170,22198],[17,21100,21094],{"id":21101},"sharing-components-between-multiple-nuxt-projects",[27,21103,21104],{},[30,21105,21106,36,21108,40,21110],{},[33,21107],{"value":35},[33,21109],{"value":39},[42,21111,21112],{},[45,21113,21114],{"href":47},[33,21115],{"value":50},[52,21117],{":tags":54},[56,21119],{":audio-src":21120,":transcript-src":21121},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F10\u002F22\u002Fsharing-components-between-multiple-nuxt-projects\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2022\u002F10\u002F22\u002Fsharing-components-between-multiple-nuxt-projects\u002Fsummary.json",[27,21123,21124],{},[63,21125],{"alt":12847,"src":21126},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1666399302\u002Fblog\u002Fnuxt-extends\u002Fnuxt-extends",[27,21128,21129,21130,21135],{},"Nuxt 3 and its new foundation come with many powerful new features, such as the new server engine ",[45,21131,21134],{"href":21132,"target":2716,"rel":21133},"https:\u002F\u002Fv3.nuxtjs.org\u002Fguide\u002Fconcepts\u002Fserver-engine\u002F",[2718,2719],"Nitro"," that made the framework lighter and faster and supported by Vue's 3 Composition API and Vite.",[27,21137,21138,21139,21144],{},"But one of Nuxt's features that not many people talk about is the ability to merge two or more applications and share functionalities between them. The ",[45,21140,21143],{"href":21141,"target":2716,"rel":21142},"https:\u002F\u002Fv3.nuxtjs.org\u002Fapi\u002Fconfiguration\u002Fnuxt-config\u002F#extends",[2718,2719],"extends"," feature in Nuxt 3 allows us to set a relative config path or remote git repositories such as GitHub, GitLab, Bitbucket or https:\u002F\u002F pointing to the source directories of a project.",[27,21146,21147],{},"This feature is a perfect use case for complex projects such us developing applications with a few different pieces, for example, an Admin and a User area that are designed separately but share the same components.",[27,21149,21150],{},"Let's see how this works with an example.",[104,21152,21154],{"id":21153},"setting-up-a-base-nuxt-3-project","Setting up a base Nuxt 3 project",[27,21156,21157],{},"Create a new Nuxt project:",[128,21159,21161],{"className":8665,"code":21160,"language":8667,"meta":133,"style":133},"npx nuxi init nuxt-base\n",[22,21162,21163],{"__ignoreMap":133},[137,21164,21165,21168,21171,21173],{"class":139,"line":140},[137,21166,21167],{"class":147},"npx",[137,21169,21170],{"class":284}," nuxi",[137,21172,9539],{"class":284},[137,21174,21175],{"class":284}," nuxt-base\n",[27,21177,21178,21179,21182],{},"Navigate to the ",[22,21180,21181],{},"nuxt-base"," directory and install the dependencies:",[128,21184,21186],{"className":8665,"code":21185,"language":8667,"meta":133,"style":133},"cd nuxt-base\nnpm install\n",[22,21187,21188,21194],{"__ignoreMap":133},[137,21189,21190,21192],{"class":139,"line":140},[137,21191,9558],{"class":364},[137,21193,21175],{"class":284},[137,21195,21196,21198],{"class":139,"line":173},[137,21197,9536],{"class":147},[137,21199,9571],{"class":284},[27,21201,21202,21203,1017],{},"We will come back to this project a bit later. Next, let's create the other Nuxt project ",[22,21204,21205],{},"nuxt-components",[104,21207,21209],{"id":21208},"setting-up-a-second-nuxt-3-project","Setting up a second Nuxt 3 project",[27,21211,21212],{},"Let's repeat what we've done previously with the new project.",[128,21214,21216],{"className":8665,"code":21215,"language":8667,"meta":133,"style":133},"npx nuxi init nuxt-components\ncd nuxt-components\nnpm install\n",[22,21217,21218,21229,21235],{"__ignoreMap":133},[137,21219,21220,21222,21224,21226],{"class":139,"line":140},[137,21221,21167],{"class":147},[137,21223,21170],{"class":284},[137,21225,9539],{"class":284},[137,21227,21228],{"class":284}," nuxt-components\n",[137,21230,21231,21233],{"class":139,"line":173},[137,21232,9558],{"class":364},[137,21234,21228],{"class":284},[137,21236,21237,21239],{"class":139,"line":188},[137,21238,9536],{"class":147},[137,21240,9571],{"class":284},[27,21242,21243],{},"We should create both projects in the same folder:",[128,21245,21247],{"className":5633,"code":21246,"language":5635,"meta":133,"style":133},"\u002Froot\n    \u002Fnuxt-base\n        app.vue\n        nuxt.config.ts\n        package.json\n        ...\n    \u002Fnuxt-components\n        app.vue\n        nuxt.config.ts\n        package.json\n        ...\n",[22,21248,21249,21254,21259,21264,21269,21273,21278,21283,21287,21291,21295],{"__ignoreMap":133},[137,21250,21251],{"class":139,"line":140},[137,21252,21253],{},"\u002Froot\n",[137,21255,21256],{"class":139,"line":173},[137,21257,21258],{},"    \u002Fnuxt-base\n",[137,21260,21261],{"class":139,"line":188},[137,21262,21263],{},"        app.vue\n",[137,21265,21266],{"class":139,"line":269},[137,21267,21268],{},"        nuxt.config.ts\n",[137,21270,21271],{"class":139,"line":278},[137,21272,9045],{},[137,21274,21275],{"class":139,"line":291},[137,21276,21277],{},"        ...\n",[137,21279,21280],{"class":139,"line":297},[137,21281,21282],{},"    \u002Fnuxt-components\n",[137,21284,21285],{"class":139,"line":302},[137,21286,21263],{},[137,21288,21289],{"class":139,"line":662},[137,21290,21268],{},[137,21292,21293],{"class":139,"line":667},[137,21294,9045],{},[137,21296,21297],{"class":139,"line":786},[137,21298,21277],{},[27,21300,21301,21302,21304,21305,21307],{},"We will create a directory called components in the root of the ",[22,21303,21205],{}," project. Inside we will add two ",[22,21306,7575],{}," files:",[27,21309,21310,21313],{},[22,21311,21312],{},"random-message.vue"," containing the following code:",[128,21315,21317],{"className":130,"code":21316,"language":132,"meta":133,"style":133},"\u003Ctemplate>\n    \u003Cdiv>\n        \u003Ch1>{{ msg }}\u003C\u002Fh1>\n        \u003Cbutton @click=\"changeMsg\">Generate a Message\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { ref } from \"vue\";\nconst msg = ref(\"\");\nconst changeMsg = () => {\n    msg.value = makeid(getRandomInt(100));\n};\n\nfunction makeid(length) {\n    var result = \"\";\n    var characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\";\n    var charactersLength = characters.length;\n    for (var i = 0; i \u003C length; i++) {\n        result += characters.charAt(Math.floor(Math.random() * charactersLength));\n    }\n    return result;\n}\n\nfunction getRandomInt(max) {\n    return Math.floor(Math.random() * max);\n}\n\u003C\u002Fscript>\n",[22,21318,21319,21327,21335,21347,21362,21367,21371,21375,21384,21388,21393,21398,21419,21423,21427,21432,21443,21455,21469,21493,21498,21502,21507,21511,21515,21520,21538,21542],{"__ignoreMap":133},[137,21320,21321,21323,21325],{"class":139,"line":140},[137,21322,4033],{"class":157},[137,21324,7821],{"class":4036},[137,21326,4053],{"class":157},[137,21328,21329,21331,21333],{"class":139,"line":173},[137,21330,4072],{"class":157},[137,21332,8330],{"class":4036},[137,21334,4053],{"class":157},[137,21336,21337,21339,21341,21343,21345],{"class":139,"line":188},[137,21338,9826],{"class":157},[137,21340,17],{"class":4036},[137,21342,9701],{"class":157},[137,21344,17],{"class":4036},[137,21346,4053],{"class":157},[137,21348,21349,21351,21353,21356,21359],{"class":139,"line":269},[137,21350,9826],{"class":157},[137,21352,8170],{"class":4036},[137,21354,21355],{"class":8180}," @click=\"changeMsg\">Generate",[137,21357,21358],{"class":147}," a",[137,21360,21361],{"class":8180}," Message\u003C\u002Fbutton>\n",[137,21363,21364],{"class":139,"line":278},[137,21365,21366],{"class":8180},"    \u003C\u002Fdiv>\n",[137,21368,21369],{"class":139,"line":291},[137,21370,8189],{"class":8180},[137,21372,21373],{"class":139,"line":297},[137,21374,516],{"emptyLinePlaceholder":515},[137,21376,21377,21380,21382],{"class":139,"line":302},[137,21378,21379],{"class":8180},"\u003Cscript",[137,21381,9642],{"class":147},[137,21383,4053],{"class":157},[137,21385,21386],{"class":139,"line":662},[137,21387,12653],{"class":157},[137,21389,21390],{"class":139,"line":667},[137,21391,21392],{"class":157},"const msg = ref(\"\");\n",[137,21394,21395],{"class":139,"line":786},[137,21396,21397],{"class":157},"const changeMsg = () => {\n",[137,21399,21400,21403,21405,21408,21410,21413,21415,21417],{"class":139,"line":798},[137,21401,21402],{"class":157},"    msg.value ",[137,21404,253],{"class":143},[137,21406,21407],{"class":147}," makeid",[137,21409,356],{"class":157},[137,21411,21412],{"class":147},"getRandomInt",[137,21414,356],{"class":157},[137,21416,2077],{"class":364},[137,21418,8614],{"class":157},[137,21420,21421],{"class":139,"line":803},[137,21422,191],{"class":157},[137,21424,21425],{"class":139,"line":931},[137,21426,516],{"emptyLinePlaceholder":515},[137,21428,21429],{"class":139,"line":1568},[137,21430,21431],{"class":157},"function makeid(length) {\n",[137,21433,21434,21437,21439,21441],{"class":139,"line":1573},[137,21435,21436],{"class":157},"    var result ",[137,21438,253],{"class":143},[137,21440,4607],{"class":284},[137,21442,3276],{"class":157},[137,21444,21445,21448,21450,21453],{"class":139,"line":1578},[137,21446,21447],{"class":157},"    var characters ",[137,21449,253],{"class":143},[137,21451,21452],{"class":284}," \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\"",[137,21454,3276],{"class":157},[137,21456,21457,21460,21462,21465,21467],{"class":139,"line":1588},[137,21458,21459],{"class":157},"    var charactersLength ",[137,21461,253],{"class":143},[137,21463,21464],{"class":157}," characters.",[137,21466,8611],{"class":364},[137,21468,3276],{"class":157},[137,21470,21471,21474,21477,21479,21481,21484,21486,21489,21491],{"class":139,"line":1601},[137,21472,21473],{"class":147},"    for",[137,21475,21476],{"class":157}," (var i ",[137,21478,253],{"class":143},[137,21480,7687],{"class":364},[137,21482,21483],{"class":157},"; i ",[137,21485,4033],{"class":143},[137,21487,21488],{"class":157}," length; i",[137,21490,12683],{"class":143},[137,21492,170],{"class":157},[137,21494,21495],{"class":139,"line":3802},[137,21496,21497],{"class":157},"        result += characters.charAt(Math.floor(Math.random() * charactersLength));\n",[137,21499,21500],{"class":139,"line":3808},[137,21501,294],{"class":157},[137,21503,21504],{"class":139,"line":3822},[137,21505,21506],{"class":157},"    return result;\n",[137,21508,21509],{"class":139,"line":3827},[137,21510,510],{"class":157},[137,21512,21513],{"class":139,"line":3832},[137,21514,516],{"emptyLinePlaceholder":515},[137,21516,21517],{"class":139,"line":3840},[137,21518,21519],{"class":157},"function getRandomInt(max) {\n",[137,21521,21522,21525,21527,21529,21531,21533,21535],{"class":139,"line":3846},[137,21523,21524],{"class":157},"    return Math.",[137,21526,8011],{"class":147},[137,21528,8014],{"class":157},[137,21530,7678],{"class":147},[137,21532,3348],{"class":157},[137,21534,7672],{"class":143},[137,21536,21537],{"class":157}," max);\n",[137,21539,21540],{"class":139,"line":3861},[137,21541,510],{"class":157},[137,21543,21544,21546,21548],{"class":139,"line":3883},[137,21545,4083],{"class":157},[137,21547,4037],{"class":4036},[137,21549,4053],{"class":157},[27,21551,21552,21553,21313],{},"And ",[22,21554,21555],{},"random-word.vue",[128,21557,21559],{"className":130,"code":21558,"language":132,"meta":133,"style":133},"\u003Ctemplate>\n    \u003Cdiv>\n        \u003Ch1>{{ word }}\u003C\u002Fh1>\n        \u003Cbutton @click=\"generateRandomWord\">Generate a Word\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { ref } from \"vue\";\nimport randomWords from \"random-words\";\n\nconst word = ref(\"\");\n\nfunction generateRandomWord() {\n    word.value = randomWords();\n}\n\u003C\u002Fscript>\n",[22,21560,21561,21569,21577,21590,21604,21608,21612,21616,21624,21628,21633,21637,21642,21646,21651,21663,21667],{"__ignoreMap":133},[137,21562,21563,21565,21567],{"class":139,"line":140},[137,21564,4033],{"class":157},[137,21566,7821],{"class":4036},[137,21568,4053],{"class":157},[137,21570,21571,21573,21575],{"class":139,"line":173},[137,21572,4072],{"class":157},[137,21574,8330],{"class":4036},[137,21576,4053],{"class":157},[137,21578,21579,21581,21583,21586,21588],{"class":139,"line":188},[137,21580,9826],{"class":157},[137,21582,17],{"class":4036},[137,21584,21585],{"class":157},">{{ word }}\u003C\u002F",[137,21587,17],{"class":4036},[137,21589,4053],{"class":157},[137,21591,21592,21594,21596,21599,21601],{"class":139,"line":269},[137,21593,9826],{"class":157},[137,21595,8170],{"class":4036},[137,21597,21598],{"class":8180}," @click=\"generateRandomWord\">Generate",[137,21600,21358],{"class":147},[137,21602,21603],{"class":8180}," Word\u003C\u002Fbutton>\n",[137,21605,21606],{"class":139,"line":278},[137,21607,21366],{"class":8180},[137,21609,21610],{"class":139,"line":291},[137,21611,8189],{"class":8180},[137,21613,21614],{"class":139,"line":297},[137,21615,516],{"emptyLinePlaceholder":515},[137,21617,21618,21620,21622],{"class":139,"line":302},[137,21619,21379],{"class":8180},[137,21621,9642],{"class":147},[137,21623,4053],{"class":157},[137,21625,21626],{"class":139,"line":662},[137,21627,12653],{"class":157},[137,21629,21630],{"class":139,"line":667},[137,21631,21632],{"class":157},"import randomWords from \"random-words\";\n",[137,21634,21635],{"class":139,"line":786},[137,21636,516],{"emptyLinePlaceholder":515},[137,21638,21639],{"class":139,"line":798},[137,21640,21641],{"class":157},"const word = ref(\"\");\n",[137,21643,21644],{"class":139,"line":803},[137,21645,516],{"emptyLinePlaceholder":515},[137,21647,21648],{"class":139,"line":931},[137,21649,21650],{"class":157},"function generateRandomWord() {\n",[137,21652,21653,21656,21658,21661],{"class":139,"line":1568},[137,21654,21655],{"class":157},"    word.value ",[137,21657,253],{"class":143},[137,21659,21660],{"class":147}," randomWords",[137,21662,924],{"class":157},[137,21664,21665],{"class":139,"line":1573},[137,21666,510],{"class":157},[137,21668,21669,21671,21673],{"class":139,"line":1578},[137,21670,4083],{"class":157},[137,21672,4037],{"class":4036},[137,21674,4053],{"class":157},[27,21676,21677,21678,21681],{},"We also need to install the ",[22,21679,21680],{},"random-word"," npm package so let's do it:",[128,21683,21685],{"className":8665,"code":21684,"language":8667,"meta":133,"style":133},"npm install random-words\n",[22,21686,21687],{"__ignoreMap":133},[137,21688,21689,21691,21693],{"class":139,"line":140},[137,21690,9536],{"class":147},[137,21692,10268],{"class":284},[137,21694,21695],{"class":284}," random-words\n",[27,21697,21698,21699,21701],{},"The idea with these two components we just created above is to use them in our ",[22,21700,21181],{}," project.",[27,21703,21704,21705,21708,21709,9772],{},"Before we move to the ",[22,21706,21707],{},"next-base"," project again, let's add configuration in the ",[22,21710,21711],{},"nuxt.config.ts",[128,21713,21715],{"className":13299,"code":21714,"language":13301,"meta":133,"style":133},"export default defineNuxtConfig({\n    components: [{ path: \".\u002Fcomponents\", prefix: \"UI\" }],\n});\n",[22,21716,21717,21729,21746],{"__ignoreMap":133},[137,21718,21719,21721,21724,21727],{"class":139,"line":140},[137,21720,13456],{"class":143},[137,21722,21723],{"class":143}," default",[137,21725,21726],{"class":147}," defineNuxtConfig",[137,21728,3175],{"class":157},[137,21730,21731,21734,21737,21740,21743],{"class":139,"line":173},[137,21732,21733],{"class":157},"    components: [{ path: ",[137,21735,21736],{"class":284},"\".\u002Fcomponents\"",[137,21738,21739],{"class":157},", prefix: ",[137,21741,21742],{"class":284},"\"UI\"",[137,21744,21745],{"class":157}," }],\n",[137,21747,21748],{"class":139,"line":188},[137,21749,5422],{"class":157},[27,21751,21752,21753,21756,21757,21760],{},"We added a prefix ",[22,21754,21755],{},"UI"," to all components in the ",[22,21758,21759],{},"\u002Fcomponents"," directory. We can use these two components like this:",[128,21762,21764],{"className":13299,"code":21763,"language":13301,"meta":133,"style":133},"\u003CUIRandomMessage \u002F>\n\u003CUIRandomWord \u002F>\n",[22,21765,21766,21776],{"__ignoreMap":133},[137,21767,21768,21770,21773],{"class":139,"line":140},[137,21769,4033],{"class":143},[137,21771,21772],{"class":157},"UIRandomMessage ",[137,21774,21775],{"class":143},"\u002F>\n",[137,21777,21778,21780,21783],{"class":139,"line":173},[137,21779,4033],{"class":143},[137,21781,21782],{"class":157},"UIRandomWord ",[137,21784,21775],{"class":143},[104,21786,21788,21789,114,21791,21793,21794,21796],{"id":21787},"adding-random-messagevue-and-random-wordvue-to-the-nuxt-base-project","Adding ",[22,21790,21312],{},[22,21792,21555],{}," to the ",[22,21795,21181],{}," project",[27,21798,4286,21799,21802,21803,21805,21806,114,21808,21810],{},[22,21800,21801],{},"app.vue"," file inside the ",[22,21804,21181],{}," project, we will use ",[22,21807,21312],{},[22,21809,21555],{}," components.",[27,21812,16255,21813,21815],{},[22,21814,21801],{}," file should look like the following:",[128,21817,21819],{"className":130,"code":21818,"language":132,"meta":133,"style":133},"\u003Ctemplate>\n    \u003Cdiv>\n        \u003CUIRandomMessage \u002F>\n        \u003CUIRandomWord \u002F>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,21820,21821,21829,21837,21846,21855,21863],{"__ignoreMap":133},[137,21822,21823,21825,21827],{"class":139,"line":140},[137,21824,4033],{"class":157},[137,21826,7821],{"class":4036},[137,21828,4053],{"class":157},[137,21830,21831,21833,21835],{"class":139,"line":173},[137,21832,4072],{"class":157},[137,21834,8330],{"class":4036},[137,21836,4053],{"class":157},[137,21838,21839,21841,21844],{"class":139,"line":188},[137,21840,9826],{"class":157},[137,21842,21843],{"class":364},"UIRandomMessage",[137,21845,4078],{"class":157},[137,21847,21848,21850,21853],{"class":139,"line":269},[137,21849,9826],{"class":157},[137,21851,21852],{"class":364},"UIRandomWord",[137,21854,4078],{"class":157},[137,21856,21857,21859,21861],{"class":139,"line":278},[137,21858,8374],{"class":157},[137,21860,8330],{"class":4036},[137,21862,4053],{"class":157},[137,21864,21865,21867,21869],{"class":139,"line":291},[137,21866,4083],{"class":157},[137,21868,7821],{"class":4036},[137,21870,4053],{"class":157},[27,21872,21873,21874,21876,21877,21879,21880,9772],{},"Next, we need to tell Nuxt that we want to extend our ",[22,21875,21181],{}," project with ",[22,21878,21205],{},". We can do so by adding the following in the ",[22,21881,21711],{},[128,21883,21885],{"className":13299,"code":21884,"language":13301,"meta":133,"style":133},"export default defineNuxtConfig({\n    components: true,\n    extends: [\"..\u002Fnuxt-components\"],\n});\n",[22,21886,21887,21897,21906,21917],{"__ignoreMap":133},[137,21888,21889,21891,21893,21895],{"class":139,"line":140},[137,21890,13456],{"class":143},[137,21892,21723],{"class":143},[137,21894,21726],{"class":147},[137,21896,3175],{"class":157},[137,21898,21899,21902,21904],{"class":139,"line":173},[137,21900,21901],{"class":157},"    components: ",[137,21903,3097],{"class":364},[137,21905,1961],{"class":157},[137,21907,21908,21911,21914],{"class":139,"line":188},[137,21909,21910],{"class":157},"    extends: [",[137,21912,21913],{"class":284},"\"..\u002Fnuxt-components\"",[137,21915,21916],{"class":157},"],\n",[137,21918,21919],{"class":139,"line":269},[137,21920,5422],{"class":157},[27,21922,21923],{},"And that's it. We can run our application and see two components in action:",[128,21925,21927],{"className":8665,"code":21926,"language":8667,"meta":133,"style":133},"npm run dev\n",[22,21928,21929],{"__ignoreMap":133},[137,21930,21931,21933,21935],{"class":139,"line":140},[137,21932,9536],{"class":147},[137,21934,9578],{"class":284},[137,21936,9581],{"class":284},[27,21938,21939],{},[63,21940],{"alt":65,"src":21941},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fv1666403383\u002Fblog\u002Fnuxt-extends\u002Fnuxt-extends-640gif_ryloin",[104,21943,21945],{"id":21944},"generate-static-site","Generate static site",[27,21947,21948,21949,21952],{},"Our application works as expected, running locally on our dev server. However, there is one problem if we try to generate statically. If we run ",[22,21950,21951],{},"npm run generate",", we will get the following error:",[128,21954,21956],{"className":8665,"code":21955,"language":8667,"meta":133,"style":133},"ERROR  [vite]: Rollup failed to resolve import \"vue\u002Fserver-renderer\" from \"..\u002Fnuxt-components\u002Fcomponents\u002Frandom-message.vue\".\nThis is most likely unintended because it can break your application at runtime.\nIf you do want to externalize this module explicitly add it to\n`build.rollupOptions.external`\n",[22,21957,21958,21977,22018,22052],{"__ignoreMap":133},[137,21959,21960,21963,21966,21969,21971,21974],{"class":139,"line":140},[137,21961,21962],{"class":147},"ERROR",[137,21964,21965],{"class":157},"  [vite]: Rollup failed to resolve import ",[137,21967,21968],{"class":284},"\"vue\u002Fserver-renderer\"",[137,21970,20514],{"class":157},[137,21972,21973],{"class":284},"\"..\u002Fnuxt-components\u002Fcomponents\u002Frandom-message.vue\"",[137,21975,21976],{"class":157},".\n",[137,21978,21979,21982,21985,21988,21991,21994,21997,22000,22003,22006,22009,22012,22015],{"class":139,"line":173},[137,21980,21981],{"class":147},"This",[137,21983,21984],{"class":284}," is",[137,21986,21987],{"class":284}," most",[137,21989,21990],{"class":284}," likely",[137,21992,21993],{"class":284}," unintended",[137,21995,21996],{"class":284}," because",[137,21998,21999],{"class":284}," it",[137,22001,22002],{"class":284}," can",[137,22004,22005],{"class":284}," break",[137,22007,22008],{"class":284}," your",[137,22010,22011],{"class":284}," application",[137,22013,22014],{"class":284}," at",[137,22016,22017],{"class":284}," runtime.\n",[137,22019,22020,22023,22026,22029,22032,22035,22038,22040,22042,22045,22047,22049],{"class":139,"line":188},[137,22021,22022],{"class":147},"If",[137,22024,22025],{"class":284}," you",[137,22027,22028],{"class":284}," do",[137,22030,22031],{"class":284}," want",[137,22033,22034],{"class":284}," to",[137,22036,22037],{"class":284}," externalize",[137,22039,365],{"class":284},[137,22041,13180],{"class":284},[137,22043,22044],{"class":284}," explicitly",[137,22046,17266],{"class":284},[137,22048,21999],{"class":284},[137,22050,22051],{"class":284}," to\n",[137,22053,22054,22057,22060],{"class":139,"line":269},[137,22055,22056],{"class":284},"`",[137,22058,22059],{"class":147},"build.rollupOptions.external",[137,22061,22062],{"class":284},"`\n",[27,22064,22065,22066,22069,22070,22072],{},"To fix this issue, we need to create a ",[22,22067,22068],{},"vite.config.ts"," file in the root of our ",[22,22071,21181],{}," project and add the following code:",[128,22074,22076],{"className":8665,"code":22075,"language":8667,"meta":133,"style":133},"import { defineConfig } from \"vite\";\n\nexport default defineConfig({\n    build: {\n        rollupOptions: {\n            external: [\"vue\u002Fserver-renderer\"],\n        },\n    },\n});\n",[22,22077,22078,22098,22102,22109,22116,22123,22135,22139,22143],{"__ignoreMap":133},[137,22079,22080,22082,22085,22088,22091,22093,22096],{"class":139,"line":140},[137,22081,10287],{"class":147},[137,22083,22084],{"class":284}," {",[137,22086,22087],{"class":284}," defineConfig",[137,22089,22090],{"class":284}," }",[137,22092,10293],{"class":284},[137,22094,22095],{"class":284}," \"vite\"",[137,22097,3276],{"class":157},[137,22099,22100],{"class":139,"line":173},[137,22101,516],{"emptyLinePlaceholder":515},[137,22103,22104,22106],{"class":139,"line":188},[137,22105,13456],{"class":143},[137,22107,22108],{"class":157}," default defineConfig({\n",[137,22110,22111,22114],{"class":139,"line":269},[137,22112,22113],{"class":147},"    build:",[137,22115,256],{"class":284},[137,22117,22118,22121],{"class":139,"line":278},[137,22119,22120],{"class":147},"        rollupOptions:",[137,22122,256],{"class":284},[137,22124,22125,22128,22131,22133],{"class":139,"line":291},[137,22126,22127],{"class":147},"            external:",[137,22129,22130],{"class":157}," [",[137,22132,21968],{"class":284},[137,22134,21916],{"class":157},[137,22136,22137],{"class":139,"line":297},[137,22138,2084],{"class":157},[137,22140,22141],{"class":139,"line":302},[137,22142,775],{"class":157},[137,22144,22145],{"class":139,"line":662},[137,22146,5422],{"class":157},[27,22148,22149,22150,22152],{},"That's it. Now we can generate our ",[22,22151,21181],{}," app without any problems.",[27,22154,22155,22156,164,22162,1017],{},"All examples above can be found in the following Github repositories: ",[45,22157,22160],{"href":22158,"target":2716,"rel":22159},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fnuxt-extends-example",[2718,2719],[22,22161,21181],{},[45,22163,22166],{"href":22164,"target":2716,"rel":22165},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fnuxt-component-generate-message",[2718,2719],[22,22167,21205],{},[104,22169,2567],{"id":2566},[2569,22171,22172,22175,22182,22185,22190],{},[1006,22173,22174],{},"One of Nuxt's features that not many people talk about is the ability to merge two or more applications and share functionalities between them. A perfect use case will be designing an Admin and a User area that are developed separately but share the same components.",[1006,22176,22177,22178,22181],{},"We can merge multiple projects by using the ",[45,22179,21143],{"href":21141,"target":2716,"rel":22180},[2718,2719]," feature in Nuxt 3.",[1006,22183,22184],{},"The extends feature allows us to set a relative config path or remote git repositories such as GitHub, GitLab, Bitbucket or https:\u002F\u002F pointing to the source directories of a project.",[1006,22186,22187,22188,10277],{},"To use another project inside our Nuxt app, we need to include the project's path in the ",[22,22189,21711],{},[1006,22191,22192,22193,22195,22196,10277],{},"To generate our project statically, we need to add ",[22,22194,21968],{}," to our ",[22,22197,22068],{},[2617,22199,10133],{},{"title":133,"searchDepth":173,"depth":173,"links":22201},[22202,22203,22204,22206,22207],{"id":21153,"depth":173,"text":21154},{"id":21208,"depth":173,"text":21209},{"id":21787,"depth":173,"text":22205},"Adding random-message.vue and random-word.vue to the nuxt-base project",{"id":21944,"depth":173,"text":21945},{"id":2566,"depth":173,"text":2567},"Nuxt 3 and its new foundation come with many powerful new features, such as the new server engine Nitro that made the framework lighter and faster and supported by Vue's 3 Composition API and Vite. But one of Nuxt's features that not many people talk about is the ability to merge two or more applications and share functionalities between them. The extends feature in Nuxt 3 allows us to set a relative config path or remote git repositories such as GitHub, GitLab, Bitbucket or https:\u002F\u002F pointing to the source directories of a project. This feature is a perfect use case for complex projects such us developing applications with a few different pieces, for example, an Admin and a User area that are designed separately but share the same components.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1666399302\u002Fblog\u002Fnuxt-extends\u002Fnuxt-extends",[21094,21096,8,22211,22212,5299,5300],"Components","Reusing components",{},"\u002F2022\u002F10\u002F22\u002Fsharing-components-between-multiple-nuxt-projects","22nd Oct 2022",{"title":21094,"description":22208},"2022\u002F10\u002F22\u002Fsharing-components-between-multiple-nuxt-projects","UQbXlCA5nghbmuZmvlfDKRG9vHW4A1ZjutMofmSqxOk",{"id":22220,"title":22221,"articleTags":22222,"author":11,"blog":12,"body":22226,"description":22690,"extension":2649,"image":22691,"keywords":22692,"meta":22697,"navigation":515,"path":22698,"published":22699,"readTime":278,"seo":22700,"stem":22701,"type":2662,"__hash__":22702},"content\u002F2023\u002F03\u002F19\u002Fsetting-up-global-variables-in-the-rest-client-vs-code-extension.md","Setting up global variables in the REST Client VS Code extension",[22223,22224,22225],"VSCode","BackEnd","Other",{"type":14,"value":22227,"toc":22682},[22228,22231,22245,22247,22251,22256,22263,22270,22273,22277,22283,22289,22297,22307,22318,22399,22410,22415,22419,22430,22549,22562,22567,22571,22576,22652,22659,22665,22671,22674,22676,22679],[17,22229,22221],{"id":22230},"setting-up-global-variables-in-the-rest-client-vs-code-extension",[27,22232,22233],{},[30,22234,22235,36,22237,40,22239],{},[33,22236],{"value":35},[33,22238],{"value":39},[42,22240,22241],{},[45,22242,22243],{"href":47},[33,22244],{"value":50},[52,22246],{":tags":54},[56,22248],{":audio-src":22249,":transcript-src":22250},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2023\u002F03\u002F19\u002Fsetting-up-global-variables-in-the-rest-client-vs-code-extension\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2023\u002F03\u002F19\u002Fsetting-up-global-variables-in-the-rest-client-vs-code-extension\u002Fsummary.json",[27,22252,22253],{},[63,22254],{"alt":12847,"src":22255},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1679178671\u002Fblog\u002Fsetting-up-global-variables-in-the-rest-client-vs-code-extension\u002Fhero-image_wywhks",[27,22257,22258,22259,22262],{},"REST Client is a Visual Studio Code (VS Code) extension (",[45,22260,2726],{"href":15935,"target":2716,"rel":22261},[2718,2719],") that allows you to send HTTP requests and view the responses directly in VS Code.",[27,22264,22265,22266,22269],{},"In the past, my go-to application for testing my REST API endpoints was Postman (",[45,22267,2726],{"href":15929,"target":2716,"rel":22268},[2718,2719],"). Postman has many advanced features that cannot be completely replaced with the REST Client extension. However, since you need to switch between two different apps (VS Code and Postman), I find REST Client to be more convenient 90% of the time I test my endpoints.",[27,22271,22272],{},"In this brief blog article, I will explain how to set up global variables that can be reused in different environments within the REST Client extension.",[104,22274,22276],{"id":22275},"add-the-rest-client-extension","Add the REST Client extension",[27,22278,22279,22280,22282],{},"First, we need to install the REST Client extension for Visual Studio Code. To do so, navigate to the Extensions icon on the left-hand side of VS Code and search for \"",[42,22281,15937],{},"\" in the search input. Install the extension, and then restart VS Code.",[27,22284,22285],{},[63,22286],{"alt":22287,"src":22288},"search for the REST Client Extension in Marketplace in VSCode","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1679177436\u002Fblog\u002Fsetting-up-global-variables-in-the-rest-client-vs-code-extension\u002FImage_-_1_p0uea1",[104,22290,22292,22293,22296],{"id":22291},"create-a-http-file","Create a ",[22,22294,22295],{},".http"," file",[27,22298,22299,22300,22302,22303,22306],{},"To use this extension, create a file with a ",[22,22301,22295],{}," extension in any location in the project. Let's create a file called ",[22,22304,22305],{},"request.server.http"," in the root of our project.",[27,22308,22309,22310,22313,22314,22317],{},"Inside the file, we'll create two requests: a ",[22,22311,22312],{},"GET"," request and a ",[22,22315,22316],{},"POST"," request. Both requests will have an Authorisation header with a bearer token.",[128,22319,22321],{"className":15948,"code":22320,"language":15950,"meta":133,"style":133},"### Get list of users\nGET http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fusers HTTP\u002F1.1\nContent-Type: application\u002Fjson\nAuthorization: Bearer L8qq9PZyRg6ieKGEKhZolGC0vJWLw8iEJ88DRdyOg\n\n{}\n\n### Create a user\nPOST http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fcreate-user HTTP\u002F1.1\nContent-Type: application\u002Fjson\nAuthorization: Bearer L8qq9PZyRg6ieKGEKhZolGC0vJWLw8iEJ88DRdyOg\n\n{\n    \"email\": \"test@test.com\",\n    \"firstName\": \"Foo\",\n    \"lastName\": \"Bar\"\n}\n",[22,22322,22323,22328,22333,22337,22342,22346,22350,22354,22359,22364,22368,22372,22376,22380,22385,22390,22395],{"__ignoreMap":133},[137,22324,22325],{"class":139,"line":140},[137,22326,22327],{},"### Get list of users\n",[137,22329,22330],{"class":139,"line":173},[137,22331,22332],{},"GET http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fusers HTTP\u002F1.1\n",[137,22334,22335],{"class":139,"line":188},[137,22336,15962],{},[137,22338,22339],{"class":139,"line":269},[137,22340,22341],{},"Authorization: Bearer L8qq9PZyRg6ieKGEKhZolGC0vJWLw8iEJ88DRdyOg\n",[137,22343,22344],{"class":139,"line":278},[137,22345,516],{"emptyLinePlaceholder":515},[137,22347,22348],{"class":139,"line":291},[137,22349,16501],{},[137,22351,22352],{"class":139,"line":297},[137,22353,516],{"emptyLinePlaceholder":515},[137,22355,22356],{"class":139,"line":302},[137,22357,22358],{},"### Create a user\n",[137,22360,22361],{"class":139,"line":662},[137,22362,22363],{},"POST http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fcreate-user HTTP\u002F1.1\n",[137,22365,22366],{"class":139,"line":667},[137,22367,15962],{},[137,22369,22370],{"class":139,"line":786},[137,22371,22341],{},[137,22373,22374],{"class":139,"line":798},[137,22375,516],{"emptyLinePlaceholder":515},[137,22377,22378],{"class":139,"line":803},[137,22379,15971],{},[137,22381,22382],{"class":139,"line":931},[137,22383,22384],{},"    \"email\": \"test@test.com\",\n",[137,22386,22387],{"class":139,"line":1568},[137,22388,22389],{},"    \"firstName\": \"Foo\",\n",[137,22391,22392],{"class":139,"line":1573},[137,22393,22394],{},"    \"lastName\": \"Bar\"\n",[137,22396,22397],{"class":139,"line":1578},[137,22398,510],{},[27,22400,22401,22402,22405,22406,22409],{},"In the code above, we make two requests: one to list all users and another to create a new user. However, there is a lot of repetitive code. For example, it would be nice to store the ",[22,22403,22404],{},"http:\u002F\u002Flocalhost:3000\u002Fapi"," URL and the ",[22,22407,22408],{},"token"," as global variables, which could then be reused throughout the code. Additionally, the token and URL may vary depending on the environment (e.g. local, staging, or production), so we need to be able to set different values for each environment.",[27,22411,22412,22413,10277],{},"To do this, we can set these variables in our VS Code and then replace them in our ",[22,22414,22305],{},[104,22416,22418],{"id":22417},"create-global-variables","Create global variables",[27,22420,22421,22422,22425,22426,22429],{},"If you haven't done so already, create a directory called ",[22,22423,22424],{},".vscode"," at the root of the project. Inside this directory, create a file called ",[22,22427,22428],{},"settings.json",". In this file, add the following:",[128,22431,22433],{"className":5155,"code":22432,"language":5157,"meta":133,"style":133},"{\n    \"rest-client.environmentVariables\": {\n        \"local\": {\n            \"baseUrl\": \"http:\u002F\u002Flocalhost:3000\u002Fapi\",\n            \"token\": \"L8qq9PZyRg6ieKGEKhZolGC0vJWLw8iEJ88DRdyOg\"\n        },\n        \"staging\": {\n            \"baseUrl\": \"http:\u002F\u002Flocalhost:3001\u002Fapi\",\n            \"token\": \"eyJhbGciOiJIUzI1NiJ9eyJSb2xlIjoiQWRtaW4if\"\n        },\n        \"production\": {\n            \"baseUrl\": \"http:\u002F\u002Flocalhost:3002\u002Fapi\",\n            \"token\": \"c3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsI\"\n        }\n    }\n}\n",[22,22434,22435,22439,22446,22453,22465,22475,22479,22486,22497,22506,22510,22517,22528,22537,22541,22545],{"__ignoreMap":133},[137,22436,22437],{"class":139,"line":140},[137,22438,15971],{"class":157},[137,22440,22441,22444],{"class":139,"line":173},[137,22442,22443],{"class":364},"    \"rest-client.environmentVariables\"",[137,22445,1819],{"class":157},[137,22447,22448,22451],{"class":139,"line":188},[137,22449,22450],{"class":364},"        \"local\"",[137,22452,1819],{"class":157},[137,22454,22455,22458,22460,22463],{"class":139,"line":269},[137,22456,22457],{"class":364},"            \"baseUrl\"",[137,22459,726],{"class":157},[137,22461,22462],{"class":284},"\"http:\u002F\u002Flocalhost:3000\u002Fapi\"",[137,22464,1961],{"class":157},[137,22466,22467,22470,22472],{"class":139,"line":278},[137,22468,22469],{"class":364},"            \"token\"",[137,22471,726],{"class":157},[137,22473,22474],{"class":284},"\"L8qq9PZyRg6ieKGEKhZolGC0vJWLw8iEJ88DRdyOg\"\n",[137,22476,22477],{"class":139,"line":291},[137,22478,2084],{"class":157},[137,22480,22481,22484],{"class":139,"line":297},[137,22482,22483],{"class":364},"        \"staging\"",[137,22485,1819],{"class":157},[137,22487,22488,22490,22492,22495],{"class":139,"line":302},[137,22489,22457],{"class":364},[137,22491,726],{"class":157},[137,22493,22494],{"class":284},"\"http:\u002F\u002Flocalhost:3001\u002Fapi\"",[137,22496,1961],{"class":157},[137,22498,22499,22501,22503],{"class":139,"line":662},[137,22500,22469],{"class":364},[137,22502,726],{"class":157},[137,22504,22505],{"class":284},"\"eyJhbGciOiJIUzI1NiJ9eyJSb2xlIjoiQWRtaW4if\"\n",[137,22507,22508],{"class":139,"line":667},[137,22509,2084],{"class":157},[137,22511,22512,22515],{"class":139,"line":786},[137,22513,22514],{"class":364},"        \"production\"",[137,22516,1819],{"class":157},[137,22518,22519,22521,22523,22526],{"class":139,"line":798},[137,22520,22457],{"class":364},[137,22522,726],{"class":157},[137,22524,22525],{"class":284},"\"http:\u002F\u002Flocalhost:3002\u002Fapi\"",[137,22527,1961],{"class":157},[137,22529,22530,22532,22534],{"class":139,"line":803},[137,22531,22469],{"class":364},[137,22533,726],{"class":157},[137,22535,22536],{"class":284},"\"c3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsI\"\n",[137,22538,22539],{"class":139,"line":931},[137,22540,1966],{"class":157},[137,22542,22543],{"class":139,"line":1568},[137,22544,294],{"class":157},[137,22546,22547],{"class":139,"line":1573},[137,22548,510],{"class":157},[27,22550,22551,22552,22555,22556,114,22559,22561],{},"As we can see from the ",[22,22553,22554],{},"JSON"," file above, we added ",[22,22557,22558],{},"baseUrl",[22,22560,22408],{}," variables with different values for each environment: local, staging, and production.",[27,22563,22564,22565,10277],{},"Next, let's see how we can use those variables inside the ",[22,22566,22305],{},[104,22568,22570],{"id":22569},"replace-hard-coded-values-with-global-variables","Replace hard-coded values with global variables",[27,22572,22573,22574,10277],{},"Now that we have set our global variables, let's add them to the ",[22,22575,22305],{},[128,22577,22579],{"className":15948,"code":22578,"language":15950,"meta":133,"style":133},"### Get list of users\nGET {{baseUrl}}\u002Fusers HTTP\u002F1.1\nContent-Type: application\u002Fjson\nAuthorization: Bearer {{token}}\n\n{}\n\n### Create a user\nPOST {{baseUrl}}\u002Fapi\u002Fcreate-user HTTP\u002F1.1\nContent-Type: application\u002Fjson\nAuthorization: Bearer {{token}}\n\n{\n    \"email\": \"test@test.com\",\n    \"firstName\": \"Foo\",\n    \"lastName\": \"Bar\"\n}\n",[22,22580,22581,22585,22590,22594,22599,22603,22607,22611,22615,22620,22624,22628,22632,22636,22640,22644,22648],{"__ignoreMap":133},[137,22582,22583],{"class":139,"line":140},[137,22584,22327],{},[137,22586,22587],{"class":139,"line":173},[137,22588,22589],{},"GET {{baseUrl}}\u002Fusers HTTP\u002F1.1\n",[137,22591,22592],{"class":139,"line":188},[137,22593,15962],{},[137,22595,22596],{"class":139,"line":269},[137,22597,22598],{},"Authorization: Bearer {{token}}\n",[137,22600,22601],{"class":139,"line":278},[137,22602,516],{"emptyLinePlaceholder":515},[137,22604,22605],{"class":139,"line":291},[137,22606,16501],{},[137,22608,22609],{"class":139,"line":297},[137,22610,516],{"emptyLinePlaceholder":515},[137,22612,22613],{"class":139,"line":302},[137,22614,22358],{},[137,22616,22617],{"class":139,"line":662},[137,22618,22619],{},"POST {{baseUrl}}\u002Fapi\u002Fcreate-user HTTP\u002F1.1\n",[137,22621,22622],{"class":139,"line":667},[137,22623,15962],{},[137,22625,22626],{"class":139,"line":786},[137,22627,22598],{},[137,22629,22630],{"class":139,"line":798},[137,22631,516],{"emptyLinePlaceholder":515},[137,22633,22634],{"class":139,"line":803},[137,22635,15971],{},[137,22637,22638],{"class":139,"line":931},[137,22639,22384],{},[137,22641,22642],{"class":139,"line":1568},[137,22643,22389],{},[137,22645,22646],{"class":139,"line":1573},[137,22647,22394],{},[137,22649,22650],{"class":139,"line":1578},[137,22651,510],{},[27,22653,22654,22655,22658],{},"The last thing we need to do is choose the environment on which we want to test those requests. We can do this by calling up the Command Palette (Ctrl+Shift+P) and typing \"",[42,22656,22657],{},"Rest Client: Switch Environment","\", and then selecting the environment we want the values to be set to.",[27,22660,22661],{},[63,22662],{"alt":22663,"src":22664},"Switch Variable for and Environment in REST Client","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1679177434\u002Fblog\u002Fsetting-up-global-variables-in-the-rest-client-vs-code-extension\u002FImage_-_2_pma4vb",[27,22666,22667],{},[63,22668],{"alt":22669,"src":22670},"Choose Environment in REST Client","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1679177434\u002Fblog\u002Fsetting-up-global-variables-in-the-rest-client-vs-code-extension\u002FImage_-_3_lwn8ch.png",[27,22672,22673],{},"And that's it, we can now choose the testing environment we want within seconds.",[104,22675,2567],{"id":2566},[27,22677,22678],{},"The REST Client VS Code extension is a handy tool for testing REST API endpoints. Although it may not have all the advanced features of Postman, it is convenient to use since it is integrated into VS Code. I frequently use this extension and find setting global variables to be a big time-saver. This article outlines the steps to quickly set up and use global variables for several different environments in the REST Client extension. I hope you find this article helpful.",[2617,22680,22681],{},"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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":133,"searchDepth":173,"depth":173,"links":22683},[22684,22685,22687,22688,22689],{"id":22275,"depth":173,"text":22276},{"id":22291,"depth":173,"text":22686},"Create a .http file",{"id":22417,"depth":173,"text":22418},{"id":22569,"depth":173,"text":22570},{"id":2566,"depth":173,"text":2567},"REST Client is a Visual Studio Code (VS Code) extension that allows you to send HTTP requests and view the responses directly in VS Code. In the past, my go-to application for testing my REST API endpoints was Postman. Postman has many advanced features that cannot be completely replaced with the REST Client extension. However, since you need to switch between two different apps (VS Code and Postman), I find REST Client to be more convenient 90% of the time I test my endpoints. In this brief blog article, I will explain how to set up global variables that can be reused in different environments within the REST Client extension.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1679178671\u002Fblog\u002Fsetting-up-global-variables-in-the-rest-client-vs-code-extension\u002Fhero-image_wywhks",[22221,22693,22694,22695,15931,22696],"VS Code","Extension","Request","HTTP",{},"\u002F2023\u002F03\u002F19\u002Fsetting-up-global-variables-in-the-rest-client-vs-code-extension","19th Mar 2023",{"title":22221,"description":22690},"2023\u002F03\u002F19\u002Fsetting-up-global-variables-in-the-rest-client-vs-code-extension","ybrPpdHO12fKuIQNcauThsO_ja6YqK_ZGk61TpM-kDs",{"id":22704,"title":22705,"articleTags":22706,"author":11,"blog":12,"body":22708,"description":26530,"extension":2649,"image":26531,"keywords":26532,"meta":26538,"navigation":515,"path":26539,"published":26540,"readTime":786,"seo":26541,"stem":26542,"type":2662,"__hash__":26543},"content\u002F2023\u002F04\u002F02\u002Fsingle-sign-on-with-firebase-authentication-across-multiple-domains.md","Single Sign On (SSO) with Firebase Authentication across multiple domains",[2668,22707,12817],"Vite",{"type":14,"value":22709,"toc":26522},[22710,22713,22727,22729,22733,22738,22744,22754,22771,22775,22778,22781,22796,22808,22857,22881,22904,22923,22937,22942,23030,23040,23044,23047,23064,23079,23086,23141,23155,23160,23189,23195,23205,23234,23240,23266,23270,23277,23291,23297,23332,23339,23667,23672,23725,23729,23744,23769,23772,23780,23785,24203,24208,24264,24277,24284,24289,25253,25273,25366,25369,25431,25438,25447,25454,25460,25533,25537,25548,25565,25807,25822,25970,25973,25979,25985,25991,26374,26383,26414,26420,26487,26494,26501,26503,26506,26516,26519],[17,22711,22705],{"id":22712},"single-sign-on-sso-with-firebase-authentication-across-multiple-domains",[27,22714,22715],{},[30,22716,22717,36,22719,40,22721],{},[33,22718],{"value":35},[33,22720],{"value":39},[42,22722,22723],{},[45,22724,22725],{"href":47},[33,22726],{"value":50},[52,22728],{":tags":54},[56,22730],{":audio-src":22731,":transcript-src":22732},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2023\u002F04\u002F02\u002Fsingle-sign-on-with-firebase-authentication-across-multiple-domains\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2023\u002F04\u002F02\u002Fsingle-sign-on-with-firebase-authentication-across-multiple-domains\u002Fsummary.json",[27,22734,22735],{},[63,22736],{"alt":12847,"src":22737},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1680413367\u002Fblog\u002Fsingle-sign-on-with-firebase-authentication-across-multiple-domains\u002FSSO-with-firebase-authentication-across-multiple-domains_nil2ba",[27,22739,22740,22743],{},[45,22741,2668],{"href":16697,"target":2716,"rel":22742},[2718,2719]," is a great platform that offers a wide range of services to developers, making it easy to build, improve, and grow their apps. One of these services is Firebase Auth, which allows for easy user authentication using its JavaScript SDK.",[27,22745,22746,22747,114,22750,22753],{},"Despite Firebase Auth being an awesome service and abstracting a lot of the complex code needed to build user authentication, I recently found one limitation: Firebase Auth doesn't persist the session across multiple domains. This means that if we use the same Firebase Auth for two different domains, such as ",[22,22748,22749],{},"https:\u002F\u002Fexample1.com",[22,22751,22752],{},"https:\u002F\u002Fexample2.com",", we have to sign in to both applications on both domains independently. Currently, Firebase doesn't have a built-in feature to handle this situation out of the box.",[27,22755,22756,22757,22760,22761,114,22765,22770],{},"In the following blog article, we will show a simple workaround to overcome this limitation using an ",[22,22758,22759],{},"\u003Ciframe>",". We will demonstrate this example by creating three different applications: the first application will be called \"authentication\", where we will create the sign in logic to handle authentication. Then, we will embed the the \"authentication\" app using an iframe within our other two applications. We will use ",[45,22762,22707],{"href":22763,"target":2716,"rel":22764},"https:\u002F\u002Fvitejs.dev\u002F",[2718,2719],[45,22766,22769],{"href":22767,"target":2716,"rel":22768},"https:\u002F\u002Ftailwindcss.com\u002F",[2718,2719],"Tailwind CSS"," to help us create the apps more quickly. You don't need to be an expert in these two technologies, but some knowledge of JavaScript, TypeScript, and basic Firebase Auth is recommended in order to follow along. Without wasting too much time, let's get started!",[104,22772,22774],{"id":22773},"create-vite-app","Create Vite app",[27,22776,22777],{},"We are planning to create three projects. As mentioned in the introduction, the first project will only focus on authentication logic. The other two projects will include authentication through the iFrame.",[27,22779,22780],{},"To start, let's create our first application using Vite and set it up with TypeScript. The process of creating a Vite app will be the same for the other two applications.",[128,22782,22784],{"className":8665,"code":22783,"language":8667,"meta":133,"style":133},"yarn create vite\n",[22,22785,22786],{"__ignoreMap":133},[137,22787,22788,22790,22793],{"class":139,"line":140},[137,22789,17263],{"class":147},[137,22791,22792],{"class":284}," create",[137,22794,22795],{"class":284}," vite\n",[128,22797,22799],{"className":8665,"code":22798,"language":8667,"meta":133,"style":133},"? Project name: › authentication\n\n",[22,22800,22801],{"__ignoreMap":133},[137,22802,22803,22805],{"class":139,"line":140},[137,22804,12972],{"class":143},[137,22806,22807],{"class":157}," Project name: › authentication\n",[128,22809,22811],{"className":8665,"code":22810,"language":8667,"meta":133,"style":133},"? Select a framework: › - Use arrow-keys. Return to submit.\n❯   Vanilla\n    Vue\n    React\n    Preact\n    Lit\n    Svelte\n    Others\n",[22,22812,22813,22820,22827,22832,22837,22842,22847,22852],{"__ignoreMap":133},[137,22814,22815,22817],{"class":139,"line":140},[137,22816,12972],{"class":143},[137,22818,22819],{"class":157}," Select a framework: › - Use arrow-keys. Return to submit.\n",[137,22821,22822,22824],{"class":139,"line":173},[137,22823,12983],{"class":147},[137,22825,22826],{"class":284},"   Vanilla\n",[137,22828,22829],{"class":139,"line":188},[137,22830,22831],{"class":147},"    Vue\n",[137,22833,22834],{"class":139,"line":269},[137,22835,22836],{"class":147},"    React\n",[137,22838,22839],{"class":139,"line":278},[137,22840,22841],{"class":147},"    Preact\n",[137,22843,22844],{"class":139,"line":291},[137,22845,22846],{"class":147},"    Lit\n",[137,22848,22849],{"class":139,"line":297},[137,22850,22851],{"class":147},"    Svelte\n",[137,22853,22854],{"class":139,"line":302},[137,22855,22856],{"class":147},"    Others\n",[128,22858,22860],{"className":8665,"code":22859,"language":8667,"meta":133,"style":133},"? Select a variant: › - Use arrow-keys. Return to submit.\nJavaScript\n❯   TypeScript\n",[22,22861,22862,22869,22874],{"__ignoreMap":133},[137,22863,22864,22866],{"class":139,"line":140},[137,22865,12972],{"class":143},[137,22867,22868],{"class":157}," Select a variant: › - Use arrow-keys. Return to submit.\n",[137,22870,22871],{"class":139,"line":173},[137,22872,22873],{"class":147},"JavaScript\n",[137,22875,22876,22878],{"class":139,"line":188},[137,22877,12983],{"class":147},[137,22879,22880],{"class":284},"   TypeScript\n",[128,22882,22884],{"className":8665,"code":22883,"language":8667,"meta":133,"style":133},"cd authentication\n  yarn\n  yarn dev\n",[22,22885,22886,22893,22897],{"__ignoreMap":133},[137,22887,22888,22890],{"class":139,"line":140},[137,22889,9558],{"class":364},[137,22891,22892],{"class":284}," authentication\n",[137,22894,22895],{"class":139,"line":173},[137,22896,12991],{"class":147},[137,22898,22899,22902],{"class":139,"line":188},[137,22900,22901],{"class":147},"  yarn",[137,22903,9581],{"class":284},[27,22905,22906,22907,22909,22910,22912,22913,114,22916,22919,22920,1017],{},"Next, we need to create ",[22,22908,22068],{}," in the root directory of the project and set the port number for our authentication app to run on locally. We will set the port number to ",[22,22911,15111],{}," for this project. For the other two projects, we will set the ports to ",[22,22914,22915],{},"3001",[22,22917,22918],{},"3002",", respectively, so that each project has its own unique port number. Additionally, we need to install a plugin to enable https on ",[22,22921,22922],{},"https:\u002F\u002Flocalhost:3000",[128,22924,22926],{"className":8665,"code":22925,"language":8667,"meta":133,"style":133},"yarn add @vitejs\u002Fplugin-basic-ssl\n",[22,22927,22928],{"__ignoreMap":133},[137,22929,22930,22932,22934],{"class":139,"line":140},[137,22931,17263],{"class":147},[137,22933,17266],{"class":284},[137,22935,22936],{"class":284}," @vitejs\u002Fplugin-basic-ssl\n",[27,22938,4737,22939,22941],{},[22,22940,22068],{}," file should resemble the following:",[128,22943,22945],{"className":13299,"code":22944,"language":13301,"meta":133,"style":133},"import { defineConfig } from \"vite\";\nimport basicSsl from \"@vitejs\u002Fplugin-basic-ssl\";\n\nexport default defineConfig({\n    server: {\n        port: 3000,\n        https: true,\n    },\n    plugins: [basicSsl()],\n});\n",[22,22946,22947,22960,22974,22978,22988,22993,23002,23011,23015,23026],{"__ignoreMap":133},[137,22948,22949,22951,22954,22956,22958],{"class":139,"line":140},[137,22950,10287],{"class":143},[137,22952,22953],{"class":157}," { defineConfig } ",[137,22955,10954],{"class":143},[137,22957,22095],{"class":284},[137,22959,3276],{"class":157},[137,22961,22962,22964,22967,22969,22972],{"class":139,"line":173},[137,22963,10287],{"class":143},[137,22965,22966],{"class":157}," basicSsl ",[137,22968,10954],{"class":143},[137,22970,22971],{"class":284}," \"@vitejs\u002Fplugin-basic-ssl\"",[137,22973,3276],{"class":157},[137,22975,22976],{"class":139,"line":188},[137,22977,516],{"emptyLinePlaceholder":515},[137,22979,22980,22982,22984,22986],{"class":139,"line":269},[137,22981,13456],{"class":143},[137,22983,21723],{"class":143},[137,22985,22087],{"class":147},[137,22987,3175],{"class":157},[137,22989,22990],{"class":139,"line":278},[137,22991,22992],{"class":157},"    server: {\n",[137,22994,22995,22998,23000],{"class":139,"line":291},[137,22996,22997],{"class":157},"        port: ",[137,22999,15111],{"class":364},[137,23001,1961],{"class":157},[137,23003,23004,23007,23009],{"class":139,"line":297},[137,23005,23006],{"class":157},"        https: ",[137,23008,3097],{"class":364},[137,23010,1961],{"class":157},[137,23012,23013],{"class":139,"line":302},[137,23014,775],{"class":157},[137,23016,23017,23020,23023],{"class":139,"line":662},[137,23018,23019],{"class":157},"    plugins: [",[137,23021,23022],{"class":147},"basicSsl",[137,23024,23025],{"class":157},"()],\n",[137,23027,23028],{"class":139,"line":667},[137,23029,5422],{"class":157},[27,23031,23032,23033,23036,23037,23039],{},"To begin building our app, let's first delete all unnecessary files in the project. Within the ",[22,23034,23035],{},"\u002Fsrc"," folder, we should only keep the ",[22,23038,13114],{}," file and remove all other logic within it.",[104,23041,23043],{"id":23042},"add-tailwind-css","Add Tailwind CSS",[27,23045,23046],{},"To help us style our application layout, we are going to add Tailwind CSS.",[128,23048,23050],{"className":8665,"code":23049,"language":8667,"meta":133,"style":133},"yarn add -D tailwindcss\n",[22,23051,23052],{"__ignoreMap":133},[137,23053,23054,23056,23058,23061],{"class":139,"line":140},[137,23055,17263],{"class":147},[137,23057,17266],{"class":284},[137,23059,23060],{"class":364}," -D",[137,23062,23063],{"class":284}," tailwindcss\n",[128,23065,23067],{"className":8665,"code":23066,"language":8667,"meta":133,"style":133},"npx tailwindcss init\n",[22,23068,23069],{"__ignoreMap":133},[137,23070,23071,23073,23076],{"class":139,"line":140},[137,23072,21167],{"class":147},[137,23074,23075],{"class":284}," tailwindcss",[137,23077,23078],{"class":284}," init\n",[27,23080,23081,23082,23085],{},"We need to add the following ",[22,23083,23084],{},"tailwind.config.cjs"," inside.",[128,23087,23089],{"className":13299,"code":23088,"language":13301,"meta":133,"style":133},"module.exports = {\n    content: [\".\u002Findex.html\", \".\u002Fsrc\u002F**\u002F*.{html,ts}\"],\n    theme: {\n        extend: {},\n    },\n    plugins: [],\n};\n",[22,23090,23091,23103,23118,23123,23128,23132,23137],{"__ignoreMap":133},[137,23092,23093,23095,23097,23099,23101],{"class":139,"line":140},[137,23094,8751],{"class":364},[137,23096,1017],{"class":157},[137,23098,8756],{"class":364},[137,23100,151],{"class":143},[137,23102,256],{"class":157},[137,23104,23105,23108,23111,23113,23116],{"class":139,"line":173},[137,23106,23107],{"class":157},"    content: [",[137,23109,23110],{"class":284},"\".\u002Findex.html\"",[137,23112,164],{"class":157},[137,23114,23115],{"class":284},"\".\u002Fsrc\u002F**\u002F*.{html,ts}\"",[137,23117,21916],{"class":157},[137,23119,23120],{"class":139,"line":188},[137,23121,23122],{"class":157},"    theme: {\n",[137,23124,23125],{"class":139,"line":269},[137,23126,23127],{"class":157},"        extend: {},\n",[137,23129,23130],{"class":139,"line":278},[137,23131,775],{"class":157},[137,23133,23134],{"class":139,"line":291},[137,23135,23136],{"class":157},"    plugins: [],\n",[137,23138,23139],{"class":139,"line":297},[137,23140,191],{"class":157},[27,23142,23143,23144,114,23147,23150,23151,23154],{},"Next, we need to create two CSS files: ",[22,23145,23146],{},"input.css",[22,23148,23149],{},"output.css",", inside the ",[22,23152,23153],{},"\u002Fassets\u002Ftailwind"," directory.",[27,23156,4370,23157,23159],{},[22,23158,23146],{},", we are adding the following code:",[128,23161,23165],{"className":23162,"code":23163,"language":23164,"meta":133,"style":133},"language-css shiki shiki-themes github-light github-dark","@tailwind base;\n@tailwind components;\n@tailwind utilities;\n","css",[22,23166,23167,23175,23182],{"__ignoreMap":133},[137,23168,23169,23172],{"class":139,"line":140},[137,23170,23171],{"class":143},"@tailwind",[137,23173,23174],{"class":157}," base;\n",[137,23176,23177,23179],{"class":139,"line":173},[137,23178,23171],{"class":143},[137,23180,23181],{"class":157}," components;\n",[137,23183,23184,23186],{"class":139,"line":188},[137,23185,23171],{"class":143},[137,23187,23188],{"class":157}," utilities;\n",[27,23190,23191,23192,23194],{},"In ",[22,23193,23149],{},", we are leaving it empty.",[27,23196,23197,23198,23201,23202,1017],{},"We need to add the following code snippet inside the ",[22,23199,23200],{},"header"," section of ",[22,23203,23204],{},"index.html",[128,23206,23208],{"className":4024,"code":23207,"language":4026,"meta":133,"style":133},"\u003Clink href=\".\u002Fassets\u002Ftailwind\u002Foutput.css\" rel=\"stylesheet\" \u002F>\n",[22,23209,23210],{"__ignoreMap":133},[137,23211,23212,23214,23216,23219,23221,23224,23227,23229,23232],{"class":139,"line":140},[137,23213,4033],{"class":157},[137,23215,2726],{"class":4036},[137,23217,23218],{"class":147}," href",[137,23220,253],{"class":157},[137,23222,23223],{"class":284},"\".\u002Fassets\u002Ftailwind\u002Foutput.css\"",[137,23225,23226],{"class":147}," rel",[137,23228,253],{"class":157},[137,23230,23231],{"class":284},"\"stylesheet\"",[137,23233,4078],{"class":157},[27,23235,23236,23237,894],{},"Next, run the following command in the terminal in parallel with ",[22,23238,23239],{},"yarn dev",[128,23241,23243],{"className":8665,"code":23242,"language":8667,"meta":133,"style":133},"npx tailwindcss -i .\u002Fassets\u002Ftailwind\u002Finput.css -o .\u002Fassets\u002Ftailwind\u002Foutput.css --watch\n",[22,23244,23245],{"__ignoreMap":133},[137,23246,23247,23249,23251,23254,23257,23260,23263],{"class":139,"line":140},[137,23248,21167],{"class":147},[137,23250,23075],{"class":284},[137,23252,23253],{"class":364}," -i",[137,23255,23256],{"class":284}," .\u002Fassets\u002Ftailwind\u002Finput.css",[137,23258,23259],{"class":364}," -o",[137,23261,23262],{"class":284}," .\u002Fassets\u002Ftailwind\u002Foutput.css",[137,23264,23265],{"class":364}," --watch\n",[104,23267,23269],{"id":23268},"add-firebase-auth","Add Firebase Auth",[27,23271,23272,23273,1017],{},"Next, we will set up Firebase Auth within our project. Before proceeding, make sure that you have a Firebase account and have created a project in the ",[45,23274,2720],{"href":23275,"target":2716,"rel":23276},"https:\u002F\u002Fconsole.firebase.google.com\u002F",[2718,2719],[128,23278,23280],{"className":8665,"code":23279,"language":8667,"meta":133,"style":133},"yarn add firebase\n",[22,23281,23282],{"__ignoreMap":133},[137,23283,23284,23286,23288],{"class":139,"line":140},[137,23285,17263],{"class":147},[137,23287,17266],{"class":284},[137,23289,23290],{"class":284}," firebase\n",[27,23292,23293,23294,23296],{},"Create an ",[22,23295,13489],{}," file in the root of the project with the following variables. You will need to create a project in Firebase and go to project settings to obtain the necessary values.",[128,23298,23300],{"className":5633,"code":23299,"language":5635,"meta":133,"style":133},"VITE_API_KEY={firebase api key}\nVITE_AUTH_DOMAIN={firebase auth domain}\nVITE_PROJECT_ID={firebase project id}\nVITE_STORAGE_BUCKET={firebase storage bucket}\nVITE_MESSAGING_SENDER_ID={firebase messaging sender id}\nVITE_APP_ID={firebase app id}\n",[22,23301,23302,23307,23312,23317,23322,23327],{"__ignoreMap":133},[137,23303,23304],{"class":139,"line":140},[137,23305,23306],{},"VITE_API_KEY={firebase api key}\n",[137,23308,23309],{"class":139,"line":173},[137,23310,23311],{},"VITE_AUTH_DOMAIN={firebase auth domain}\n",[137,23313,23314],{"class":139,"line":188},[137,23315,23316],{},"VITE_PROJECT_ID={firebase project id}\n",[137,23318,23319],{"class":139,"line":269},[137,23320,23321],{},"VITE_STORAGE_BUCKET={firebase storage bucket}\n",[137,23323,23324],{"class":139,"line":278},[137,23325,23326],{},"VITE_MESSAGING_SENDER_ID={firebase messaging sender id}\n",[137,23328,23329],{"class":139,"line":291},[137,23330,23331],{},"VITE_APP_ID={firebase app id}\n",[27,23333,23334,23335,23338],{},"Next, create a ",[22,23336,23337],{},"firebase.config.ts"," file where you'll add the logic for Firebase authentication.",[128,23340,23342],{"className":13299,"code":23341,"language":13301,"meta":133,"style":133},"import { initializeApp, FirebaseApp } from \"firebase\u002Fapp\";\nimport { getAuth } from \"firebase\u002Fauth\";\n\nexport default class FirebaseConfig {\n    private firebaseConfig = {\n        apiKey: \"\",\n        authDomain: \"\",\n        projectId: \"\",\n        storageBucket: \"\",\n        messagingSenderId: \"\",\n        appId: \"\",\n    };\n\n    private app: FirebaseApp;\n\n    constructor() {\n        this.firebaseConfig = {\n            apiKey: import.meta.env.VITE_API_KEY,\n            authDomain: import.meta.env.VITE_AUTH_DOMAIN,\n            projectId: import.meta.env.VITE_PROJECT_ID,\n            storageBucket: import.meta.env.VITE_STORAGE_BUCKET,\n            messagingSenderId: import.meta.env.VITE_MESSAGING_SENDER_ID,\n            appId: import.meta.env.VITE_APP_ID,\n        };\n        this.app = initializeApp(this.firebaseConfig);\n    }\n\n    auth() {\n        return getAuth(this.app);\n    }\n}\n",[22,23343,23344,23358,23372,23376,23389,23401,23410,23419,23428,23437,23446,23455,23459,23463,23476,23480,23486,23497,23517,23535,23553,23571,23589,23607,23611,23630,23634,23638,23645,23659,23663],{"__ignoreMap":133},[137,23345,23346,23348,23351,23353,23356],{"class":139,"line":140},[137,23347,10287],{"class":143},[137,23349,23350],{"class":157}," { initializeApp, FirebaseApp } ",[137,23352,10954],{"class":143},[137,23354,23355],{"class":284}," \"firebase\u002Fapp\"",[137,23357,3276],{"class":157},[137,23359,23360,23362,23365,23367,23370],{"class":139,"line":173},[137,23361,10287],{"class":143},[137,23363,23364],{"class":157}," { getAuth } ",[137,23366,10954],{"class":143},[137,23368,23369],{"class":284}," \"firebase\u002Fauth\"",[137,23371,3276],{"class":157},[137,23373,23374],{"class":139,"line":188},[137,23375,516],{"emptyLinePlaceholder":515},[137,23377,23378,23380,23382,23384,23387],{"class":139,"line":269},[137,23379,13456],{"class":143},[137,23381,21723],{"class":143},[137,23383,7832],{"class":143},[137,23385,23386],{"class":147}," FirebaseConfig",[137,23388,256],{"class":157},[137,23390,23391,23394,23397,23399],{"class":139,"line":278},[137,23392,23393],{"class":143},"    private",[137,23395,23396],{"class":161}," firebaseConfig",[137,23398,151],{"class":143},[137,23400,256],{"class":157},[137,23402,23403,23406,23408],{"class":139,"line":291},[137,23404,23405],{"class":157},"        apiKey: ",[137,23407,4535],{"class":284},[137,23409,1961],{"class":157},[137,23411,23412,23415,23417],{"class":139,"line":297},[137,23413,23414],{"class":157},"        authDomain: ",[137,23416,4535],{"class":284},[137,23418,1961],{"class":157},[137,23420,23421,23424,23426],{"class":139,"line":302},[137,23422,23423],{"class":157},"        projectId: ",[137,23425,4535],{"class":284},[137,23427,1961],{"class":157},[137,23429,23430,23433,23435],{"class":139,"line":662},[137,23431,23432],{"class":157},"        storageBucket: ",[137,23434,4535],{"class":284},[137,23436,1961],{"class":157},[137,23438,23439,23442,23444],{"class":139,"line":667},[137,23440,23441],{"class":157},"        messagingSenderId: ",[137,23443,4535],{"class":284},[137,23445,1961],{"class":157},[137,23447,23448,23451,23453],{"class":139,"line":786},[137,23449,23450],{"class":157},"        appId: ",[137,23452,4535],{"class":284},[137,23454,1961],{"class":157},[137,23456,23457],{"class":139,"line":798},[137,23458,1892],{"class":157},[137,23460,23461],{"class":139,"line":803},[137,23462,516],{"emptyLinePlaceholder":515},[137,23464,23465,23467,23469,23471,23474],{"class":139,"line":931},[137,23466,23393],{"class":143},[137,23468,15064],{"class":161},[137,23470,894],{"class":143},[137,23472,23473],{"class":147}," FirebaseApp",[137,23475,3276],{"class":157},[137,23477,23478],{"class":139,"line":1568},[137,23479,516],{"emptyLinePlaceholder":515},[137,23481,23482,23484],{"class":139,"line":1573},[137,23483,3651],{"class":143},[137,23485,275],{"class":157},[137,23487,23488,23490,23493,23495],{"class":139,"line":1578},[137,23489,3679],{"class":364},[137,23491,23492],{"class":157},".firebaseConfig ",[137,23494,253],{"class":143},[137,23496,256],{"class":157},[137,23498,23499,23502,23504,23506,23509,23512,23515],{"class":139,"line":1588},[137,23500,23501],{"class":157},"            apiKey: ",[137,23503,10287],{"class":143},[137,23505,1017],{"class":157},[137,23507,23508],{"class":364},"meta",[137,23510,23511],{"class":157},".env.",[137,23513,23514],{"class":364},"VITE_API_KEY",[137,23516,1961],{"class":157},[137,23518,23519,23522,23524,23526,23528,23530,23533],{"class":139,"line":1601},[137,23520,23521],{"class":157},"            authDomain: ",[137,23523,10287],{"class":143},[137,23525,1017],{"class":157},[137,23527,23508],{"class":364},[137,23529,23511],{"class":157},[137,23531,23532],{"class":364},"VITE_AUTH_DOMAIN",[137,23534,1961],{"class":157},[137,23536,23537,23540,23542,23544,23546,23548,23551],{"class":139,"line":3802},[137,23538,23539],{"class":157},"            projectId: ",[137,23541,10287],{"class":143},[137,23543,1017],{"class":157},[137,23545,23508],{"class":364},[137,23547,23511],{"class":157},[137,23549,23550],{"class":364},"VITE_PROJECT_ID",[137,23552,1961],{"class":157},[137,23554,23555,23558,23560,23562,23564,23566,23569],{"class":139,"line":3808},[137,23556,23557],{"class":157},"            storageBucket: ",[137,23559,10287],{"class":143},[137,23561,1017],{"class":157},[137,23563,23508],{"class":364},[137,23565,23511],{"class":157},[137,23567,23568],{"class":364},"VITE_STORAGE_BUCKET",[137,23570,1961],{"class":157},[137,23572,23573,23576,23578,23580,23582,23584,23587],{"class":139,"line":3822},[137,23574,23575],{"class":157},"            messagingSenderId: ",[137,23577,10287],{"class":143},[137,23579,1017],{"class":157},[137,23581,23508],{"class":364},[137,23583,23511],{"class":157},[137,23585,23586],{"class":364},"VITE_MESSAGING_SENDER_ID",[137,23588,1961],{"class":157},[137,23590,23591,23594,23596,23598,23600,23602,23605],{"class":139,"line":3827},[137,23592,23593],{"class":157},"            appId: ",[137,23595,10287],{"class":143},[137,23597,1017],{"class":157},[137,23599,23508],{"class":364},[137,23601,23511],{"class":157},[137,23603,23604],{"class":364},"VITE_APP_ID",[137,23606,1961],{"class":157},[137,23608,23609],{"class":139,"line":3832},[137,23610,1507],{"class":157},[137,23612,23613,23615,23618,23620,23623,23625,23627],{"class":139,"line":3840},[137,23614,3679],{"class":364},[137,23616,23617],{"class":157},".app ",[137,23619,253],{"class":143},[137,23621,23622],{"class":147}," initializeApp",[137,23624,356],{"class":157},[137,23626,24],{"class":364},[137,23628,23629],{"class":157},".firebaseConfig);\n",[137,23631,23632],{"class":139,"line":3846},[137,23633,294],{"class":157},[137,23635,23636],{"class":139,"line":3861},[137,23637,516],{"emptyLinePlaceholder":515},[137,23639,23640,23643],{"class":139,"line":3883},[137,23641,23642],{"class":147},"    auth",[137,23644,275],{"class":157},[137,23646,23647,23649,23652,23654,23656],{"class":139,"line":3896},[137,23648,5472],{"class":143},[137,23650,23651],{"class":147}," getAuth",[137,23653,356],{"class":157},[137,23655,24],{"class":364},[137,23657,23658],{"class":157},".app);\n",[137,23660,23661],{"class":139,"line":3901},[137,23662,294],{"class":157},[137,23664,23665],{"class":139,"line":3906},[137,23666,510],{"class":157},[27,23668,23669,23670,10277],{},"We will initialise our Firebase setup in the ",[22,23671,13114],{},[128,23673,23675],{"className":13299,"code":23674,"language":13301,"meta":133,"style":133},"import FirebaseConfig from \".\u002Ffirebase.config\";\n\nconst firebase = new FirebaseConfig();\nconst auth = firebase.auth();\n",[22,23676,23677,23691,23695,23710],{"__ignoreMap":133},[137,23678,23679,23681,23684,23686,23689],{"class":139,"line":140},[137,23680,10287],{"class":143},[137,23682,23683],{"class":157}," FirebaseConfig ",[137,23685,10954],{"class":143},[137,23687,23688],{"class":284}," \".\u002Ffirebase.config\"",[137,23690,3276],{"class":157},[137,23692,23693],{"class":139,"line":173},[137,23694,516],{"emptyLinePlaceholder":515},[137,23696,23697,23699,23702,23704,23706,23708],{"class":139,"line":188},[137,23698,3077],{"class":143},[137,23700,23701],{"class":364}," firebase",[137,23703,151],{"class":143},[137,23705,1426],{"class":143},[137,23707,23386],{"class":147},[137,23709,924],{"class":157},[137,23711,23712,23714,23717,23719,23721,23723],{"class":139,"line":269},[137,23713,3077],{"class":143},[137,23715,23716],{"class":364}," auth",[137,23718,151],{"class":143},[137,23720,3085],{"class":157},[137,23722,2751],{"class":147},[137,23724,924],{"class":157},[104,23726,23728],{"id":23727},"implement-the-sign-in-logic","Implement the sign-in logic",[27,23730,4286,23731,23733,23734,23737,23738,23740,23741,1017],{},[22,23732,23204],{}," file, inside the ",[22,23735,23736],{},"\u003Cbody>"," HTML tag, create a ",[22,23739,3595],{}," with the class attribute set to ",[22,23742,23743],{},"id=\"app\"",[128,23745,23747],{"className":4024,"code":23746,"language":4026,"meta":133,"style":133},"\u003Cdiv id=\"app\">\u003C\u002Fdiv>\n",[22,23748,23749],{"__ignoreMap":133},[137,23750,23751,23753,23755,23758,23760,23763,23765,23767],{"class":139,"line":140},[137,23752,4033],{"class":157},[137,23754,8330],{"class":4036},[137,23756,23757],{"class":147}," id",[137,23759,253],{"class":157},[137,23761,23762],{"class":284},"\"app\"",[137,23764,4048],{"class":157},[137,23766,8330],{"class":4036},[137,23768,4053],{"class":157},[27,23770,23771],{},"This class is responsible for rendering the components that we define below.",[27,23773,23774,23775,15443,23777,23779],{},"We will create a directory called ",[22,23776,21759],{},[22,23778,23035],{}," folder. Within this directory, we will define the following components:",[27,23781,23782],{},[22,23783,23784],{},"sign-in-form.ts",[128,23786,23788],{"className":4024,"code":23787,"language":4026,"meta":133,"style":133},"export default function componentSignIn() { return \u002F*html*\u002F `\n\u003Cdiv class=\"mt-8 sm:mx-auto sm:w-full sm:max-w-md\">\n    \u003Cdiv class=\"bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10\">\n        \u003Cform class=\"space-y-6\" id=\"sign-in-form\">\n            \u003Cdiv>\n                \u003Clabel for=\"email\" class=\"block text-sm font-medium leading-6 text-gray-900\">Email address\u003C\u002Flabel>\n                \u003Cdiv class=\"mt-2\">\n                    \u003Cinput\n                        id=\"email\"\n                        name=\"email\"\n                        type=\"email\"\n                        autocomplete=\"email\"\n                        required\n                        class=\"px-2 block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-green-600 sm:text-sm sm:leading-6\"\n                    \u002F>\n                \u003C\u002Fdiv>\n            \u003C\u002Fdiv>\n\n            \u003Cdiv>\n                \u003Clabel for=\"password\" class=\"block text-sm font-medium leading-6 text-gray-900\">Password\u003C\u002Flabel>\n                \u003Cdiv class=\"mt-2\">\n                    \u003Cinput\n                        id=\"password\"\n                        name=\"password\"\n                        type=\"password\"\n                        autocomplete=\"current-password\"\n                        required\n                        class=\"px-2 block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-green-600 sm:text-sm sm:leading-6\"\n                    \u002F>\n                \u003C\u002Fdiv>\n            \u003C\u002Fdiv>\n\n            \u003Cdiv>\n                \u003Cbutton\n                    type=\"submit\"\n                    class=\"flex w-full justify-center rounded-md bg-green-600 py-2 px-3 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\"\n                >\n                    Sign in\n                \u003C\u002Fbutton>\n            \u003C\u002Fdiv>\n        \u003C\u002Fform>\n    \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n`; }\n",[22,23789,23790,23795,23810,23825,23848,23857,23887,23902,23910,23920,23929,23938,23947,23952,23962,23967,23976,23985,23989,23997,24023,24037,24043,24052,24060,24068,24077,24081,24089,24093,24101,24109,24113,24121,24127,24137,24147,24152,24157,24165,24173,24181,24189,24197],{"__ignoreMap":133},[137,23791,23792],{"class":139,"line":140},[137,23793,23794],{"class":157},"export default function componentSignIn() { return \u002F*html*\u002F `\n",[137,23796,23797,23799,23801,23803,23805,23808],{"class":139,"line":173},[137,23798,4033],{"class":157},[137,23800,8330],{"class":4036},[137,23802,7832],{"class":147},[137,23804,253],{"class":157},[137,23806,23807],{"class":284},"\"mt-8 sm:mx-auto sm:w-full sm:max-w-md\"",[137,23809,4053],{"class":157},[137,23811,23812,23814,23816,23818,23820,23823],{"class":139,"line":188},[137,23813,4072],{"class":157},[137,23815,8330],{"class":4036},[137,23817,7832],{"class":147},[137,23819,253],{"class":157},[137,23821,23822],{"class":284},"\"bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10\"",[137,23824,4053],{"class":157},[137,23826,23827,23829,23832,23834,23836,23839,23841,23843,23846],{"class":139,"line":269},[137,23828,9826],{"class":157},[137,23830,23831],{"class":4036},"form",[137,23833,7832],{"class":147},[137,23835,253],{"class":157},[137,23837,23838],{"class":284},"\"space-y-6\"",[137,23840,23757],{"class":147},[137,23842,253],{"class":157},[137,23844,23845],{"class":284},"\"sign-in-form\"",[137,23847,4053],{"class":157},[137,23849,23850,23853,23855],{"class":139,"line":278},[137,23851,23852],{"class":157},"            \u003C",[137,23854,8330],{"class":4036},[137,23856,4053],{"class":157},[137,23858,23859,23862,23865,23868,23870,23873,23875,23877,23880,23883,23885],{"class":139,"line":291},[137,23860,23861],{"class":157},"                \u003C",[137,23863,23864],{"class":4036},"label",[137,23866,23867],{"class":147}," for",[137,23869,253],{"class":157},[137,23871,23872],{"class":284},"\"email\"",[137,23874,7832],{"class":147},[137,23876,253],{"class":157},[137,23878,23879],{"class":284},"\"block text-sm font-medium leading-6 text-gray-900\"",[137,23881,23882],{"class":157},">Email address\u003C\u002F",[137,23884,23864],{"class":4036},[137,23886,4053],{"class":157},[137,23888,23889,23891,23893,23895,23897,23900],{"class":139,"line":297},[137,23890,23861],{"class":157},[137,23892,8330],{"class":4036},[137,23894,7832],{"class":147},[137,23896,253],{"class":157},[137,23898,23899],{"class":284},"\"mt-2\"",[137,23901,4053],{"class":157},[137,23903,23904,23907],{"class":139,"line":302},[137,23905,23906],{"class":157},"                    \u003C",[137,23908,23909],{"class":4036},"input\n",[137,23911,23912,23915,23917],{"class":139,"line":662},[137,23913,23914],{"class":147},"                        id",[137,23916,253],{"class":157},[137,23918,23919],{"class":284},"\"email\"\n",[137,23921,23922,23925,23927],{"class":139,"line":667},[137,23923,23924],{"class":147},"                        name",[137,23926,253],{"class":157},[137,23928,23919],{"class":284},[137,23930,23931,23934,23936],{"class":139,"line":786},[137,23932,23933],{"class":147},"                        type",[137,23935,253],{"class":157},[137,23937,23919],{"class":284},[137,23939,23940,23943,23945],{"class":139,"line":798},[137,23941,23942],{"class":147},"                        autocomplete",[137,23944,253],{"class":157},[137,23946,23919],{"class":284},[137,23948,23949],{"class":139,"line":803},[137,23950,23951],{"class":147},"                        required\n",[137,23953,23954,23957,23959],{"class":139,"line":931},[137,23955,23956],{"class":147},"                        class",[137,23958,253],{"class":157},[137,23960,23961],{"class":284},"\"px-2 block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-green-600 sm:text-sm sm:leading-6\"\n",[137,23963,23964],{"class":139,"line":1568},[137,23965,23966],{"class":157},"                    \u002F>\n",[137,23968,23969,23972,23974],{"class":139,"line":1573},[137,23970,23971],{"class":157},"                \u003C\u002F",[137,23973,8330],{"class":4036},[137,23975,4053],{"class":157},[137,23977,23978,23981,23983],{"class":139,"line":1578},[137,23979,23980],{"class":157},"            \u003C\u002F",[137,23982,8330],{"class":4036},[137,23984,4053],{"class":157},[137,23986,23987],{"class":139,"line":1588},[137,23988,516],{"emptyLinePlaceholder":515},[137,23990,23991,23993,23995],{"class":139,"line":1601},[137,23992,23852],{"class":157},[137,23994,8330],{"class":4036},[137,23996,4053],{"class":157},[137,23998,23999,24001,24003,24005,24007,24010,24012,24014,24016,24019,24021],{"class":139,"line":3802},[137,24000,23861],{"class":157},[137,24002,23864],{"class":4036},[137,24004,23867],{"class":147},[137,24006,253],{"class":157},[137,24008,24009],{"class":284},"\"password\"",[137,24011,7832],{"class":147},[137,24013,253],{"class":157},[137,24015,23879],{"class":284},[137,24017,24018],{"class":157},">Password\u003C\u002F",[137,24020,23864],{"class":4036},[137,24022,4053],{"class":157},[137,24024,24025,24027,24029,24031,24033,24035],{"class":139,"line":3808},[137,24026,23861],{"class":157},[137,24028,8330],{"class":4036},[137,24030,7832],{"class":147},[137,24032,253],{"class":157},[137,24034,23899],{"class":284},[137,24036,4053],{"class":157},[137,24038,24039,24041],{"class":139,"line":3822},[137,24040,23906],{"class":157},[137,24042,23909],{"class":4036},[137,24044,24045,24047,24049],{"class":139,"line":3827},[137,24046,23914],{"class":147},[137,24048,253],{"class":157},[137,24050,24051],{"class":284},"\"password\"\n",[137,24053,24054,24056,24058],{"class":139,"line":3832},[137,24055,23924],{"class":147},[137,24057,253],{"class":157},[137,24059,24051],{"class":284},[137,24061,24062,24064,24066],{"class":139,"line":3840},[137,24063,23933],{"class":147},[137,24065,253],{"class":157},[137,24067,24051],{"class":284},[137,24069,24070,24072,24074],{"class":139,"line":3846},[137,24071,23942],{"class":147},[137,24073,253],{"class":157},[137,24075,24076],{"class":284},"\"current-password\"\n",[137,24078,24079],{"class":139,"line":3861},[137,24080,23951],{"class":147},[137,24082,24083,24085,24087],{"class":139,"line":3883},[137,24084,23956],{"class":147},[137,24086,253],{"class":157},[137,24088,23961],{"class":284},[137,24090,24091],{"class":139,"line":3896},[137,24092,23966],{"class":157},[137,24094,24095,24097,24099],{"class":139,"line":3901},[137,24096,23971],{"class":157},[137,24098,8330],{"class":4036},[137,24100,4053],{"class":157},[137,24102,24103,24105,24107],{"class":139,"line":3906},[137,24104,23980],{"class":157},[137,24106,8330],{"class":4036},[137,24108,4053],{"class":157},[137,24110,24111],{"class":139,"line":3911},[137,24112,516],{"emptyLinePlaceholder":515},[137,24114,24115,24117,24119],{"class":139,"line":4666},[137,24116,23852],{"class":157},[137,24118,8330],{"class":4036},[137,24120,4053],{"class":157},[137,24122,24123,24125],{"class":139,"line":4672},[137,24124,23861],{"class":157},[137,24126,8385],{"class":4036},[137,24128,24129,24132,24134],{"class":139,"line":4680},[137,24130,24131],{"class":147},"                    type",[137,24133,253],{"class":157},[137,24135,24136],{"class":284},"\"submit\"\n",[137,24138,24139,24142,24144],{"class":139,"line":4711},[137,24140,24141],{"class":147},"                    class",[137,24143,253],{"class":157},[137,24145,24146],{"class":284},"\"flex w-full justify-center rounded-md bg-green-600 py-2 px-3 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\"\n",[137,24148,24149],{"class":139,"line":4716},[137,24150,24151],{"class":157},"                >\n",[137,24153,24154],{"class":139,"line":4721},[137,24155,24156],{"class":157},"                    Sign in\n",[137,24158,24159,24161,24163],{"class":139,"line":4727},[137,24160,23971],{"class":157},[137,24162,8170],{"class":4036},[137,24164,4053],{"class":157},[137,24166,24167,24169,24171],{"class":139,"line":4732},[137,24168,23980],{"class":157},[137,24170,8330],{"class":4036},[137,24172,4053],{"class":157},[137,24174,24175,24177,24179],{"class":139,"line":5006},[137,24176,9843],{"class":157},[137,24178,23831],{"class":4036},[137,24180,4053],{"class":157},[137,24182,24183,24185,24187],{"class":139,"line":5014},[137,24184,8374],{"class":157},[137,24186,8330],{"class":4036},[137,24188,4053],{"class":157},[137,24190,24191,24193,24195],{"class":139,"line":14343},[137,24192,4083],{"class":157},[137,24194,8330],{"class":4036},[137,24196,4053],{"class":157},[137,24198,24200],{"class":139,"line":24199},44,[137,24201,24202],{"class":157},"`; }\n",[27,24204,24205],{},[22,24206,24207],{},"signed-in.ts",[128,24209,24211],{"className":4024,"code":24210,"language":4026,"meta":133,"style":133},"export default function componentSignedIn() { return \u002F*html*\u002F `\n\u003Cdiv class=\"mt-8 sm:mx-auto sm:w-full sm:max-w-md\">\n    \u003Cdiv class=\"bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10 text-center\">User signed in.\u003C\u002Fdiv>\n\u003C\u002Fdiv>\n`; }\n",[22,24212,24213,24218,24232,24252,24260],{"__ignoreMap":133},[137,24214,24215],{"class":139,"line":140},[137,24216,24217],{"class":157},"export default function componentSignedIn() { return \u002F*html*\u002F `\n",[137,24219,24220,24222,24224,24226,24228,24230],{"class":139,"line":173},[137,24221,4033],{"class":157},[137,24223,8330],{"class":4036},[137,24225,7832],{"class":147},[137,24227,253],{"class":157},[137,24229,23807],{"class":284},[137,24231,4053],{"class":157},[137,24233,24234,24236,24238,24240,24242,24245,24248,24250],{"class":139,"line":188},[137,24235,4072],{"class":157},[137,24237,8330],{"class":4036},[137,24239,7832],{"class":147},[137,24241,253],{"class":157},[137,24243,24244],{"class":284},"\"bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10 text-center\"",[137,24246,24247],{"class":157},">User signed in.\u003C\u002F",[137,24249,8330],{"class":4036},[137,24251,4053],{"class":157},[137,24253,24254,24256,24258],{"class":139,"line":269},[137,24255,4083],{"class":157},[137,24257,8330],{"class":4036},[137,24259,4053],{"class":157},[137,24261,24262],{"class":139,"line":278},[137,24263,24202],{"class":157},[3244,24265,24266],{},[27,24267,24268,24269,24272,24273,4409],{},"To properly highlight HTML in template literals in JavaScript, you can use the ",[22,24270,24271],{},"es6-string-html"," VSCode extension (",[45,24274,2726],{"href":24275,"target":2716,"rel":24276},"https:\u002F\u002Fmarketplace.visualstudio.com\u002Fitems?itemName=Tobermory.es6-string-html",[2718,2719],[27,24278,24279,24280,24283],{},"In addition, we need to define two utility classes that will be inside the ",[22,24281,24282],{},"\u002Fsrc\u002Futils\u002Findex.ts"," file. The first utility function will be a type predicate that determines if an error is a Firebase Error. The second utility function will parse the Firebase error into a human-readable string. You can find the implementation of these utility functions in the GitHub repo, which we will link below.",[27,24285,24286,24287,9772],{},"Next, we will define the logic inside the ",[22,24288,13114],{},[128,24290,24292],{"className":13299,"code":24291,"language":13301,"meta":133,"style":133},"import FirebaseConfig from \".\u002Ffirebase.config\";\nimport { onAuthStateChanged, User } from \"firebase\u002Fauth\";\nimport { parseFirebaseError, isFirebaseError } from \".\u002Futils\";\nimport { signInWithEmailAndPassword, signOut } from \"firebase\u002Fauth\";\nimport componentSignIn from \".\u002Fcomponents\u002Fsign-in-from\";\nimport componentSignedIn from \".\u002Fcomponents\u002Fsigned-in\";\n\nconst firebase = new FirebaseConfig();\nconst auth = firebase.auth();\n\nconst app = document.getElementById(\"app\") as HTMLIFrameElement;\n\nfunction setSignInComponent() {\n    app.innerHTML = componentSignIn();\n}\n\nfunction setSignedInComponent() {\n    app.innerHTML = componentSignedIn();\n}\n\nfunction setSignInSubmitListener(signInForm: HTMLFormElement) {\n    signInForm.addEventListener(\"submit\", (e) => {\n        e.preventDefault();\n        const email = signInForm[\"email\"].value;\n        const password = signInForm[\"password\"].value;\n        signUserIn(email, password);\n    });\n}\n\nfunction removeSignInSubmitListener(signInForm: HTMLFormElement) {\n    signInForm?.removeEventListener(\"submit\", () => {});\n}\n\nlet signedInUser: User | null = null;\n\nwindow.onmessage = function (event) {\n    if (event.origin === \"https:\u002F\u002Flocalhost:3001\" || event.origin === \"https:\u002F\u002Flocalhost:3002\") {\n        if (event.data === \"signOut\") {\n            signUserOut();\n        }\n        if (event.data === \"getUserInfo\") {\n            sendUserInfo();\n        }\n    }\n};\n\nfunction sendUserInfo() {\n    window.parent.postMessage(JSON.parse(JSON.stringify(signedInUser)), \"https:\u002F\u002Flocalhost:3001\");\n    window.parent.postMessage(JSON.parse(JSON.stringify(signedInUser)), \"https:\u002F\u002Flocalhost:3002\");\n}\n\nonAuthStateChanged(auth, (user) => {\n    if (user) {\n        signedInUser = user;\n        removeSignInSubmitListener(document.getElementById(\"sign-in-form\") as HTMLFormElement);\n        setSignedInComponent();\n        sendUserInfo();\n    } else {\n        setSignInComponent();\n        setSignInSubmitListener(document.getElementById(\"sign-in-form\") as HTMLFormElement);\n        sendUserInfo();\n    }\n});\n\nasync function signUserIn(email: string, password: string) {\n    try {\n        await signInWithEmailAndPassword(auth, email, password);\n    } catch (error) {\n        if (isFirebaseError(error)) {\n            const readableError = parseFirebaseError(error.message);\n            console.error(readableError);\n        } else {\n            console.error(error);\n        }\n    }\n}\n\nasync function signUserOut() {\n    signedInUser = null;\n    try {\n        await signOut(auth);\n    } catch (error) {\n        if (isFirebaseError(error)) {\n            const readableError = parseFirebaseError(error.message);\n            console.error(readableError);\n        } else {\n            console.error(error);\n        }\n    }\n}\n",[22,24293,24294,24306,24319,24333,24346,24360,24374,24378,24392,24406,24410,24437,24441,24450,24462,24466,24470,24479,24490,24494,24498,24517,24541,24551,24568,24583,24591,24595,24599,24603,24620,24639,24643,24647,24669,24673,24692,24718,24732,24739,24743,24756,24763,24767,24771,24776,24781,24791,24825,24855,24860,24865,24882,24890,24900,24923,24931,24939,24950,24958,24980,24987,24992,24997,25002,25030,25038,25050,25059,25072,25087,25097,25106,25116,25121,25126,25131,25136,25148,25160,25167,25178,25187,25198,25211,25220,25229,25238,25243,25248],{"__ignoreMap":133},[137,24295,24296,24298,24300,24302,24304],{"class":139,"line":140},[137,24297,10287],{"class":143},[137,24299,23683],{"class":157},[137,24301,10954],{"class":143},[137,24303,23688],{"class":284},[137,24305,3276],{"class":157},[137,24307,24308,24310,24313,24315,24317],{"class":139,"line":173},[137,24309,10287],{"class":143},[137,24311,24312],{"class":157}," { onAuthStateChanged, User } ",[137,24314,10954],{"class":143},[137,24316,23369],{"class":284},[137,24318,3276],{"class":157},[137,24320,24321,24323,24326,24328,24331],{"class":139,"line":188},[137,24322,10287],{"class":143},[137,24324,24325],{"class":157}," { parseFirebaseError, isFirebaseError } ",[137,24327,10954],{"class":143},[137,24329,24330],{"class":284}," \".\u002Futils\"",[137,24332,3276],{"class":157},[137,24334,24335,24337,24340,24342,24344],{"class":139,"line":269},[137,24336,10287],{"class":143},[137,24338,24339],{"class":157}," { signInWithEmailAndPassword, signOut } ",[137,24341,10954],{"class":143},[137,24343,23369],{"class":284},[137,24345,3276],{"class":157},[137,24347,24348,24350,24353,24355,24358],{"class":139,"line":278},[137,24349,10287],{"class":143},[137,24351,24352],{"class":157}," componentSignIn ",[137,24354,10954],{"class":143},[137,24356,24357],{"class":284}," \".\u002Fcomponents\u002Fsign-in-from\"",[137,24359,3276],{"class":157},[137,24361,24362,24364,24367,24369,24372],{"class":139,"line":291},[137,24363,10287],{"class":143},[137,24365,24366],{"class":157}," componentSignedIn ",[137,24368,10954],{"class":143},[137,24370,24371],{"class":284}," \".\u002Fcomponents\u002Fsigned-in\"",[137,24373,3276],{"class":157},[137,24375,24376],{"class":139,"line":297},[137,24377,516],{"emptyLinePlaceholder":515},[137,24379,24380,24382,24384,24386,24388,24390],{"class":139,"line":302},[137,24381,3077],{"class":143},[137,24383,23701],{"class":364},[137,24385,151],{"class":143},[137,24387,1426],{"class":143},[137,24389,23386],{"class":147},[137,24391,924],{"class":157},[137,24393,24394,24396,24398,24400,24402,24404],{"class":139,"line":662},[137,24395,3077],{"class":143},[137,24397,23716],{"class":364},[137,24399,151],{"class":143},[137,24401,3085],{"class":157},[137,24403,2751],{"class":147},[137,24405,924],{"class":157},[137,24407,24408],{"class":139,"line":667},[137,24409,516],{"emptyLinePlaceholder":515},[137,24411,24412,24414,24416,24418,24420,24423,24425,24427,24429,24432,24435],{"class":139,"line":786},[137,24413,3077],{"class":143},[137,24415,15064],{"class":364},[137,24417,151],{"class":143},[137,24419,3717],{"class":157},[137,24421,24422],{"class":147},"getElementById",[137,24424,356],{"class":157},[137,24426,23762],{"class":284},[137,24428,219],{"class":157},[137,24430,24431],{"class":143},"as",[137,24433,24434],{"class":147}," HTMLIFrameElement",[137,24436,3276],{"class":157},[137,24438,24439],{"class":139,"line":798},[137,24440,516],{"emptyLinePlaceholder":515},[137,24442,24443,24445,24448],{"class":139,"line":803},[137,24444,483],{"class":143},[137,24446,24447],{"class":147}," setSignInComponent",[137,24449,275],{"class":157},[137,24451,24452,24455,24457,24460],{"class":139,"line":931},[137,24453,24454],{"class":157},"    app.innerHTML ",[137,24456,253],{"class":143},[137,24458,24459],{"class":147}," componentSignIn",[137,24461,924],{"class":157},[137,24463,24464],{"class":139,"line":1568},[137,24465,510],{"class":157},[137,24467,24468],{"class":139,"line":1573},[137,24469,516],{"emptyLinePlaceholder":515},[137,24471,24472,24474,24477],{"class":139,"line":1578},[137,24473,483],{"class":143},[137,24475,24476],{"class":147}," setSignedInComponent",[137,24478,275],{"class":157},[137,24480,24481,24483,24485,24488],{"class":139,"line":1588},[137,24482,24454],{"class":157},[137,24484,253],{"class":143},[137,24486,24487],{"class":147}," componentSignedIn",[137,24489,924],{"class":157},[137,24491,24492],{"class":139,"line":1601},[137,24493,510],{"class":157},[137,24495,24496],{"class":139,"line":3802},[137,24497,516],{"emptyLinePlaceholder":515},[137,24499,24500,24502,24505,24507,24510,24512,24515],{"class":139,"line":3808},[137,24501,483],{"class":143},[137,24503,24504],{"class":147}," setSignInSubmitListener",[137,24506,356],{"class":157},[137,24508,24509],{"class":161},"signInForm",[137,24511,894],{"class":143},[137,24513,24514],{"class":147}," HTMLFormElement",[137,24516,170],{"class":157},[137,24518,24519,24522,24524,24526,24529,24532,24535,24537,24539],{"class":139,"line":3822},[137,24520,24521],{"class":157},"    signInForm.",[137,24523,4412],{"class":147},[137,24525,356],{"class":157},[137,24527,24528],{"class":284},"\"submit\"",[137,24530,24531],{"class":157},", (",[137,24533,24534],{"class":161},"e",[137,24536,219],{"class":157},[137,24538,222],{"class":143},[137,24540,256],{"class":157},[137,24542,24543,24546,24549],{"class":139,"line":3827},[137,24544,24545],{"class":157},"        e.",[137,24547,24548],{"class":147},"preventDefault",[137,24550,924],{"class":157},[137,24552,24553,24555,24558,24560,24563,24565],{"class":139,"line":3832},[137,24554,3008],{"class":143},[137,24556,24557],{"class":364}," email",[137,24559,151],{"class":143},[137,24561,24562],{"class":157}," signInForm[",[137,24564,23872],{"class":284},[137,24566,24567],{"class":157},"].value;\n",[137,24569,24570,24572,24575,24577,24579,24581],{"class":139,"line":3840},[137,24571,3008],{"class":143},[137,24573,24574],{"class":364}," password",[137,24576,151],{"class":143},[137,24578,24562],{"class":157},[137,24580,24009],{"class":284},[137,24582,24567],{"class":157},[137,24584,24585,24588],{"class":139,"line":3846},[137,24586,24587],{"class":147},"        signUserIn",[137,24589,24590],{"class":157},"(email, password);\n",[137,24592,24593],{"class":139,"line":3861},[137,24594,2832],{"class":157},[137,24596,24597],{"class":139,"line":3883},[137,24598,510],{"class":157},[137,24600,24601],{"class":139,"line":3896},[137,24602,516],{"emptyLinePlaceholder":515},[137,24604,24605,24607,24610,24612,24614,24616,24618],{"class":139,"line":3901},[137,24606,483],{"class":143},[137,24608,24609],{"class":147}," removeSignInSubmitListener",[137,24611,356],{"class":157},[137,24613,24509],{"class":161},[137,24615,894],{"class":143},[137,24617,24514],{"class":147},[137,24619,170],{"class":157},[137,24621,24622,24625,24628,24630,24632,24634,24636],{"class":139,"line":3906},[137,24623,24624],{"class":157},"    signInForm?.",[137,24626,24627],{"class":147},"removeEventListener",[137,24629,356],{"class":157},[137,24631,24528],{"class":284},[137,24633,4420],{"class":157},[137,24635,222],{"class":143},[137,24637,24638],{"class":157}," {});\n",[137,24640,24641],{"class":139,"line":3911},[137,24642,510],{"class":157},[137,24644,24645],{"class":139,"line":4666},[137,24646,516],{"emptyLinePlaceholder":515},[137,24648,24649,24651,24654,24656,24659,24661,24663,24665,24667],{"class":139,"line":4672},[137,24650,11498],{"class":143},[137,24652,24653],{"class":157}," signedInUser",[137,24655,894],{"class":143},[137,24657,24658],{"class":147}," User",[137,24660,14113],{"class":143},[137,24662,3417],{"class":364},[137,24664,151],{"class":143},[137,24666,3417],{"class":364},[137,24668,3276],{"class":157},[137,24670,24671],{"class":139,"line":4680},[137,24672,516],{"emptyLinePlaceholder":515},[137,24674,24675,24678,24681,24683,24685,24687,24690],{"class":139,"line":4711},[137,24676,24677],{"class":157},"window.",[137,24679,24680],{"class":147},"onmessage",[137,24682,151],{"class":143},[137,24684,154],{"class":143},[137,24686,158],{"class":157},[137,24688,24689],{"class":161},"event",[137,24691,170],{"class":157},[137,24693,24694,24697,24700,24702,24705,24708,24711,24713,24716],{"class":139,"line":4716},[137,24695,24696],{"class":143},"    if",[137,24698,24699],{"class":157}," (event.origin ",[137,24701,5502],{"class":143},[137,24703,24704],{"class":284}," \"https:\u002F\u002Flocalhost:3001\"",[137,24706,24707],{"class":143}," ||",[137,24709,24710],{"class":157}," event.origin ",[137,24712,5502],{"class":143},[137,24714,24715],{"class":284}," \"https:\u002F\u002Flocalhost:3002\"",[137,24717,170],{"class":157},[137,24719,24720,24722,24725,24727,24730],{"class":139,"line":4721},[137,24721,5496],{"class":143},[137,24723,24724],{"class":157}," (event.data ",[137,24726,5502],{"class":143},[137,24728,24729],{"class":284}," \"signOut\"",[137,24731,170],{"class":157},[137,24733,24734,24737],{"class":139,"line":4727},[137,24735,24736],{"class":147},"            signUserOut",[137,24738,924],{"class":157},[137,24740,24741],{"class":139,"line":4732},[137,24742,1966],{"class":157},[137,24744,24745,24747,24749,24751,24754],{"class":139,"line":5006},[137,24746,5496],{"class":143},[137,24748,24724],{"class":157},[137,24750,5502],{"class":143},[137,24752,24753],{"class":284}," \"getUserInfo\"",[137,24755,170],{"class":157},[137,24757,24758,24761],{"class":139,"line":5014},[137,24759,24760],{"class":147},"            sendUserInfo",[137,24762,924],{"class":157},[137,24764,24765],{"class":139,"line":14343},[137,24766,1966],{"class":157},[137,24768,24769],{"class":139,"line":24199},[137,24770,294],{"class":157},[137,24772,24774],{"class":139,"line":24773},45,[137,24775,191],{"class":157},[137,24777,24779],{"class":139,"line":24778},46,[137,24780,516],{"emptyLinePlaceholder":515},[137,24782,24784,24786,24789],{"class":139,"line":24783},47,[137,24785,483],{"class":143},[137,24787,24788],{"class":147}," sendUserInfo",[137,24790,275],{"class":157},[137,24792,24794,24797,24800,24802,24804,24806,24808,24810,24812,24814,24817,24820,24823],{"class":139,"line":24793},48,[137,24795,24796],{"class":157},"    window.parent.",[137,24798,24799],{"class":147},"postMessage",[137,24801,356],{"class":157},[137,24803,22554],{"class":364},[137,24805,1017],{"class":157},[137,24807,17441],{"class":147},[137,24809,356],{"class":157},[137,24811,22554],{"class":364},[137,24813,1017],{"class":157},[137,24815,24816],{"class":147},"stringify",[137,24818,24819],{"class":157},"(signedInUser)), ",[137,24821,24822],{"class":284},"\"https:\u002F\u002Flocalhost:3001\"",[137,24824,1502],{"class":157},[137,24826,24828,24830,24832,24834,24836,24838,24840,24842,24844,24846,24848,24850,24853],{"class":139,"line":24827},49,[137,24829,24796],{"class":157},[137,24831,24799],{"class":147},[137,24833,356],{"class":157},[137,24835,22554],{"class":364},[137,24837,1017],{"class":157},[137,24839,17441],{"class":147},[137,24841,356],{"class":157},[137,24843,22554],{"class":364},[137,24845,1017],{"class":157},[137,24847,24816],{"class":147},[137,24849,24819],{"class":157},[137,24851,24852],{"class":284},"\"https:\u002F\u002Flocalhost:3002\"",[137,24854,1502],{"class":157},[137,24856,24858],{"class":139,"line":24857},50,[137,24859,510],{"class":157},[137,24861,24863],{"class":139,"line":24862},51,[137,24864,516],{"emptyLinePlaceholder":515},[137,24866,24868,24871,24874,24876,24878,24880],{"class":139,"line":24867},52,[137,24869,24870],{"class":147},"onAuthStateChanged",[137,24872,24873],{"class":157},"(auth, (",[137,24875,13193],{"class":161},[137,24877,219],{"class":157},[137,24879,222],{"class":143},[137,24881,256],{"class":157},[137,24883,24885,24887],{"class":139,"line":24884},53,[137,24886,24696],{"class":143},[137,24888,24889],{"class":157}," (user) {\n",[137,24891,24893,24896,24898],{"class":139,"line":24892},54,[137,24894,24895],{"class":157},"        signedInUser ",[137,24897,253],{"class":143},[137,24899,15724],{"class":157},[137,24901,24903,24906,24909,24911,24913,24915,24917,24919,24921],{"class":139,"line":24902},55,[137,24904,24905],{"class":147},"        removeSignInSubmitListener",[137,24907,24908],{"class":157},"(document.",[137,24910,24422],{"class":147},[137,24912,356],{"class":157},[137,24914,23845],{"class":284},[137,24916,219],{"class":157},[137,24918,24431],{"class":143},[137,24920,24514],{"class":147},[137,24922,1502],{"class":157},[137,24924,24926,24929],{"class":139,"line":24925},56,[137,24927,24928],{"class":147},"        setSignedInComponent",[137,24930,924],{"class":157},[137,24932,24934,24937],{"class":139,"line":24933},57,[137,24935,24936],{"class":147},"        sendUserInfo",[137,24938,924],{"class":157},[137,24940,24942,24945,24948],{"class":139,"line":24941},58,[137,24943,24944],{"class":157},"    } ",[137,24946,24947],{"class":143},"else",[137,24949,256],{"class":157},[137,24951,24953,24956],{"class":139,"line":24952},59,[137,24954,24955],{"class":147},"        setSignInComponent",[137,24957,924],{"class":157},[137,24959,24961,24964,24966,24968,24970,24972,24974,24976,24978],{"class":139,"line":24960},60,[137,24962,24963],{"class":147},"        setSignInSubmitListener",[137,24965,24908],{"class":157},[137,24967,24422],{"class":147},[137,24969,356],{"class":157},[137,24971,23845],{"class":284},[137,24973,219],{"class":157},[137,24975,24431],{"class":143},[137,24977,24514],{"class":147},[137,24979,1502],{"class":157},[137,24981,24983,24985],{"class":139,"line":24982},61,[137,24984,24936],{"class":147},[137,24986,924],{"class":157},[137,24988,24990],{"class":139,"line":24989},62,[137,24991,294],{"class":157},[137,24993,24995],{"class":139,"line":24994},63,[137,24996,5422],{"class":157},[137,24998,25000],{"class":139,"line":24999},64,[137,25001,516],{"emptyLinePlaceholder":515},[137,25003,25005,25007,25009,25012,25014,25016,25018,25020,25022,25024,25026,25028],{"class":139,"line":25004},65,[137,25006,15050],{"class":143},[137,25008,154],{"class":143},[137,25010,25011],{"class":147}," signUserIn",[137,25013,356],{"class":157},[137,25015,15569],{"class":161},[137,25017,894],{"class":143},[137,25019,13630],{"class":364},[137,25021,164],{"class":157},[137,25023,15574],{"class":161},[137,25025,894],{"class":143},[137,25027,13630],{"class":364},[137,25029,170],{"class":157},[137,25031,25033,25036],{"class":139,"line":25032},66,[137,25034,25035],{"class":143},"    try",[137,25037,256],{"class":157},[137,25039,25041,25044,25047],{"class":139,"line":25040},67,[137,25042,25043],{"class":143},"        await",[137,25045,25046],{"class":147}," signInWithEmailAndPassword",[137,25048,25049],{"class":157},"(auth, email, password);\n",[137,25051,25053,25055,25057],{"class":139,"line":25052},68,[137,25054,24944],{"class":157},[137,25056,2807],{"class":143},[137,25058,15734],{"class":157},[137,25060,25062,25064,25066,25069],{"class":139,"line":25061},69,[137,25063,5496],{"class":143},[137,25065,158],{"class":157},[137,25067,25068],{"class":147},"isFirebaseError",[137,25070,25071],{"class":157},"(error)) {\n",[137,25073,25075,25077,25080,25082,25085],{"class":139,"line":25074},70,[137,25076,5772],{"class":143},[137,25078,25079],{"class":364}," readableError",[137,25081,151],{"class":143},[137,25083,25084],{"class":147}," parseFirebaseError",[137,25086,15747],{"class":157},[137,25088,25090,25092,25094],{"class":139,"line":25089},71,[137,25091,1493],{"class":157},[137,25093,2812],{"class":147},[137,25095,25096],{"class":157},"(readableError);\n",[137,25098,25100,25102,25104],{"class":139,"line":25099},72,[137,25101,15729],{"class":157},[137,25103,24947],{"class":143},[137,25105,256],{"class":157},[137,25107,25109,25111,25113],{"class":139,"line":25108},73,[137,25110,1493],{"class":157},[137,25112,2812],{"class":147},[137,25114,25115],{"class":157},"(error);\n",[137,25117,25119],{"class":139,"line":25118},74,[137,25120,1966],{"class":157},[137,25122,25124],{"class":139,"line":25123},75,[137,25125,294],{"class":157},[137,25127,25129],{"class":139,"line":25128},76,[137,25130,510],{"class":157},[137,25132,25134],{"class":139,"line":25133},77,[137,25135,516],{"emptyLinePlaceholder":515},[137,25137,25139,25141,25143,25146],{"class":139,"line":25138},78,[137,25140,15050],{"class":143},[137,25142,154],{"class":143},[137,25144,25145],{"class":147}," signUserOut",[137,25147,275],{"class":157},[137,25149,25151,25154,25156,25158],{"class":139,"line":25150},79,[137,25152,25153],{"class":157},"    signedInUser ",[137,25155,253],{"class":143},[137,25157,3417],{"class":364},[137,25159,3276],{"class":157},[137,25161,25163,25165],{"class":139,"line":25162},80,[137,25164,25035],{"class":143},[137,25166,256],{"class":157},[137,25168,25170,25172,25175],{"class":139,"line":25169},81,[137,25171,25043],{"class":143},[137,25173,25174],{"class":147}," signOut",[137,25176,25177],{"class":157},"(auth);\n",[137,25179,25181,25183,25185],{"class":139,"line":25180},82,[137,25182,24944],{"class":157},[137,25184,2807],{"class":143},[137,25186,15734],{"class":157},[137,25188,25190,25192,25194,25196],{"class":139,"line":25189},83,[137,25191,5496],{"class":143},[137,25193,158],{"class":157},[137,25195,25068],{"class":147},[137,25197,25071],{"class":157},[137,25199,25201,25203,25205,25207,25209],{"class":139,"line":25200},84,[137,25202,5772],{"class":143},[137,25204,25079],{"class":364},[137,25206,151],{"class":143},[137,25208,25084],{"class":147},[137,25210,15747],{"class":157},[137,25212,25214,25216,25218],{"class":139,"line":25213},85,[137,25215,1493],{"class":157},[137,25217,2812],{"class":147},[137,25219,25096],{"class":157},[137,25221,25223,25225,25227],{"class":139,"line":25222},86,[137,25224,15729],{"class":157},[137,25226,24947],{"class":143},[137,25228,256],{"class":157},[137,25230,25232,25234,25236],{"class":139,"line":25231},87,[137,25233,1493],{"class":157},[137,25235,2812],{"class":147},[137,25237,25115],{"class":157},[137,25239,25241],{"class":139,"line":25240},88,[137,25242,1966],{"class":157},[137,25244,25246],{"class":139,"line":25245},89,[137,25247,294],{"class":157},[137,25249,25251],{"class":139,"line":25250},90,[137,25252,510],{"class":157},[27,25254,25255,25256,25259,25260,114,25263,25266,25267,25269,25270,25272],{},"As shown in the code above, we have defined the ",[22,25257,25258],{},"window.onmessage"," event. This event listens for messages that will be executed from the iFrames we are going to define in the next section. We listen for two events: ",[22,25261,25262],{},"signOut",[22,25264,25265],{},"getUserInfo",", and we execute the relevant functions. For example, when the iFrame sends a ",[22,25268,25262],{}," event, we will log the user out. Similarly, when we receive a ",[22,25271,25265],{}," event, we will send the current user's details to the iFrame. We will learn more about these two events when we start implementing the logic in the iFrames in the next section.",[128,25274,25276],{"className":13299,"code":25275,"language":13301,"meta":133,"style":133},"window.onmessage = function (event) {\n    if (event.origin === \"https:\u002F\u002Flocalhost:3001\" || event.origin === \"https:\u002F\u002Flocalhost:3002\") {\n        if (event.data === \"signOut\") {\n            signUserOut();\n        }\n        if (event.data === \"getUserInfo\") {\n            sendUserInfo();\n        }\n    }\n};\n",[22,25277,25278,25294,25314,25326,25332,25336,25348,25354,25358,25362],{"__ignoreMap":133},[137,25279,25280,25282,25284,25286,25288,25290,25292],{"class":139,"line":140},[137,25281,24677],{"class":157},[137,25283,24680],{"class":147},[137,25285,151],{"class":143},[137,25287,154],{"class":143},[137,25289,158],{"class":157},[137,25291,24689],{"class":161},[137,25293,170],{"class":157},[137,25295,25296,25298,25300,25302,25304,25306,25308,25310,25312],{"class":139,"line":173},[137,25297,24696],{"class":143},[137,25299,24699],{"class":157},[137,25301,5502],{"class":143},[137,25303,24704],{"class":284},[137,25305,24707],{"class":143},[137,25307,24710],{"class":157},[137,25309,5502],{"class":143},[137,25311,24715],{"class":284},[137,25313,170],{"class":157},[137,25315,25316,25318,25320,25322,25324],{"class":139,"line":188},[137,25317,5496],{"class":143},[137,25319,24724],{"class":157},[137,25321,5502],{"class":143},[137,25323,24729],{"class":284},[137,25325,170],{"class":157},[137,25327,25328,25330],{"class":139,"line":269},[137,25329,24736],{"class":147},[137,25331,924],{"class":157},[137,25333,25334],{"class":139,"line":278},[137,25335,1966],{"class":157},[137,25337,25338,25340,25342,25344,25346],{"class":139,"line":291},[137,25339,5496],{"class":143},[137,25341,24724],{"class":157},[137,25343,5502],{"class":143},[137,25345,24753],{"class":284},[137,25347,170],{"class":157},[137,25349,25350,25352],{"class":139,"line":297},[137,25351,24760],{"class":147},[137,25353,924],{"class":157},[137,25355,25356],{"class":139,"line":302},[137,25357,1966],{"class":157},[137,25359,25360],{"class":139,"line":662},[137,25361,294],{"class":157},[137,25363,25364],{"class":139,"line":667},[137,25365,191],{"class":157},[27,25367,25368],{},"We also send user details when the authentication state changes, for example, when a user successfully signs in or signs out.",[128,25370,25372],{"className":13299,"code":25371,"language":13301,"meta":133,"style":133},"onAuthStateChanged(auth, (user) => {\n    if (user) {\n                ...\n        sendUserInfo();\n    } else {\n                ...\n        sendUserInfo();\n    }\n});\n",[22,25373,25374,25388,25394,25399,25405,25413,25417,25423,25427],{"__ignoreMap":133},[137,25375,25376,25378,25380,25382,25384,25386],{"class":139,"line":140},[137,25377,24870],{"class":147},[137,25379,24873],{"class":157},[137,25381,13193],{"class":161},[137,25383,219],{"class":157},[137,25385,222],{"class":143},[137,25387,256],{"class":157},[137,25389,25390,25392],{"class":139,"line":173},[137,25391,24696],{"class":143},[137,25393,24889],{"class":157},[137,25395,25396],{"class":139,"line":188},[137,25397,25398],{"class":143},"                ...\n",[137,25400,25401,25403],{"class":139,"line":269},[137,25402,24936],{"class":147},[137,25404,924],{"class":157},[137,25406,25407,25409,25411],{"class":139,"line":278},[137,25408,24944],{"class":157},[137,25410,24947],{"class":143},[137,25412,256],{"class":157},[137,25414,25415],{"class":139,"line":291},[137,25416,25398],{"class":143},[137,25418,25419,25421],{"class":139,"line":297},[137,25420,24936],{"class":147},[137,25422,924],{"class":157},[137,25424,25425],{"class":139,"line":302},[137,25426,294],{"class":157},[137,25428,25429],{"class":139,"line":662},[137,25430,5422],{"class":157},[27,25432,25433,25434,25437],{},"It's important to note that we use ",[22,25435,25436],{},"JSON.parse(JSON.stringify(signedInUser))",". If we were to send the plain object without deep cloning it, we would get the following error:",[128,25439,25441],{"className":5633,"code":25440,"language":5635,"meta":133,"style":133},"It turns out the object I passed had methods, which is why the error message said An object could not be cloned.\n",[22,25442,25443],{"__ignoreMap":133},[137,25444,25445],{"class":139,"line":140},[137,25446,25440],{},[27,25448,25449,25450,25453],{},"This is because the Firebase user object we passed had methods, which is why the error message said \"",[22,25451,25452],{},"An object could not be cloned","\".",[27,25455,25456,25457,1017],{},"To fix this, we simply wrap our user object with ",[22,25458,25459],{},"JSON.parse(JSON.stringify())",[128,25461,25463],{"className":13299,"code":25462,"language":13301,"meta":133,"style":133},"function sendUserInfo() {\n    window.parent.postMessage(JSON.parse(JSON.stringify(signedInUser)), \"https:\u002F\u002Flocalhost:3001\");\n    window.parent.postMessage(JSON.parse(JSON.stringify(signedInUser)), \"https:\u002F\u002Flocalhost:3002\");\n}\n",[22,25464,25465,25473,25501,25529],{"__ignoreMap":133},[137,25466,25467,25469,25471],{"class":139,"line":140},[137,25468,483],{"class":143},[137,25470,24788],{"class":147},[137,25472,275],{"class":157},[137,25474,25475,25477,25479,25481,25483,25485,25487,25489,25491,25493,25495,25497,25499],{"class":139,"line":173},[137,25476,24796],{"class":157},[137,25478,24799],{"class":147},[137,25480,356],{"class":157},[137,25482,22554],{"class":364},[137,25484,1017],{"class":157},[137,25486,17441],{"class":147},[137,25488,356],{"class":157},[137,25490,22554],{"class":364},[137,25492,1017],{"class":157},[137,25494,24816],{"class":147},[137,25496,24819],{"class":157},[137,25498,24822],{"class":284},[137,25500,1502],{"class":157},[137,25502,25503,25505,25507,25509,25511,25513,25515,25517,25519,25521,25523,25525,25527],{"class":139,"line":188},[137,25504,24796],{"class":157},[137,25506,24799],{"class":147},[137,25508,356],{"class":157},[137,25510,22554],{"class":364},[137,25512,1017],{"class":157},[137,25514,17441],{"class":147},[137,25516,356],{"class":157},[137,25518,22554],{"class":364},[137,25520,1017],{"class":157},[137,25522,24816],{"class":147},[137,25524,24819],{"class":157},[137,25526,24852],{"class":284},[137,25528,1502],{"class":157},[137,25530,25531],{"class":139,"line":269},[137,25532,510],{"class":157},[104,25534,25536],{"id":25535},"create-a-project-to-embed-the-authentication-logic","Create a project to embed the authentication logic",[27,25538,25539,25540,25543,25544,25547],{},"As we did previously, we need to create a new Vite app called ",[22,25541,25542],{},"iFrame-1"," that will be hosted on ",[22,25545,25546],{},"http:\u002F\u002Flocalhost:3001",". The process is the same as before, and instructions are provided above. However, we do not need to install the Firebase dependency this time because all communication with Firebase will be done between the iFrame.",[27,25549,25550,25551,25553,25554,25556,25557,25560,25561,25564],{},"The difference this time is that we will set up two ",[22,25552,8330],{}," elements in the ",[22,25555,23204],{}," file: ",[22,25558,25559],{},"navigation",", where we will load the navigation component, and ",[22,25562,25563],{},"sign-in-iframe",", where we will load the sign-in iframe that we created in the previous section.",[128,25566,25568],{"className":4024,"code":25567,"language":4026,"meta":133,"style":133},"\u003C!doctype html>\n\u003Chtml lang=\"en\">\n    \u003Chead>\n        \u003Cmeta charset=\"UTF-8\" \u002F>\n        \u003Clink rel=\"icon\" type=\"image\u002Fsvg+xml\" href=\"\u002Fvite.svg\" \u002F>\n        \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" \u002F>\n        \u003Ctitle>iFrame - 1\u003C\u002Ftitle>\n        \u003Clink href=\".\u002Fassets\u002Ftailwind\u002Foutput.css\" rel=\"stylesheet\" \u002F>\n    \u003C\u002Fhead>\n    \u003Cbody>\n        \u003Cdiv id=\"navigation\">\u003C\u002Fdiv>\n        \u003Cdiv id=\"sign-in-iframe\">\u003C\u002Fdiv>\n        \u003Cscript type=\"module\" src=\"\u002Fsrc\u002Fmain.ts\">\u003C\u002Fscript>\n    \u003C\u002Fbody>\n\u003C\u002Fhtml>\n",[22,25569,25570,25583,25599,25608,25624,25654,25677,25691,25711,25719,25727,25746,25765,25791,25799],{"__ignoreMap":133},[137,25571,25572,25575,25578,25581],{"class":139,"line":140},[137,25573,25574],{"class":157},"\u003C!",[137,25576,25577],{"class":4036},"doctype",[137,25579,25580],{"class":147}," html",[137,25582,4053],{"class":157},[137,25584,25585,25587,25589,25592,25594,25597],{"class":139,"line":173},[137,25586,4033],{"class":157},[137,25588,4026],{"class":4036},[137,25590,25591],{"class":147}," lang",[137,25593,253],{"class":157},[137,25595,25596],{"class":284},"\"en\"",[137,25598,4053],{"class":157},[137,25600,25601,25603,25606],{"class":139,"line":188},[137,25602,4072],{"class":157},[137,25604,25605],{"class":4036},"head",[137,25607,4053],{"class":157},[137,25609,25610,25612,25614,25617,25619,25622],{"class":139,"line":269},[137,25611,9826],{"class":157},[137,25613,23508],{"class":4036},[137,25615,25616],{"class":147}," charset",[137,25618,253],{"class":157},[137,25620,25621],{"class":284},"\"UTF-8\"",[137,25623,4078],{"class":157},[137,25625,25626,25628,25630,25632,25634,25637,25640,25642,25645,25647,25649,25652],{"class":139,"line":278},[137,25627,9826],{"class":157},[137,25629,2726],{"class":4036},[137,25631,23226],{"class":147},[137,25633,253],{"class":157},[137,25635,25636],{"class":284},"\"icon\"",[137,25638,25639],{"class":147}," type",[137,25641,253],{"class":157},[137,25643,25644],{"class":284},"\"image\u002Fsvg+xml\"",[137,25646,23218],{"class":147},[137,25648,253],{"class":157},[137,25650,25651],{"class":284},"\"\u002Fvite.svg\"",[137,25653,4078],{"class":157},[137,25655,25656,25658,25660,25662,25664,25667,25670,25672,25675],{"class":139,"line":291},[137,25657,9826],{"class":157},[137,25659,23508],{"class":4036},[137,25661,891],{"class":147},[137,25663,253],{"class":157},[137,25665,25666],{"class":284},"\"viewport\"",[137,25668,25669],{"class":147}," content",[137,25671,253],{"class":157},[137,25673,25674],{"class":284},"\"width=device-width, initial-scale=1.0\"",[137,25676,4078],{"class":157},[137,25678,25679,25681,25684,25687,25689],{"class":139,"line":297},[137,25680,9826],{"class":157},[137,25682,25683],{"class":4036},"title",[137,25685,25686],{"class":157},">iFrame - 1\u003C\u002F",[137,25688,25683],{"class":4036},[137,25690,4053],{"class":157},[137,25692,25693,25695,25697,25699,25701,25703,25705,25707,25709],{"class":139,"line":302},[137,25694,9826],{"class":157},[137,25696,2726],{"class":4036},[137,25698,23218],{"class":147},[137,25700,253],{"class":157},[137,25702,23223],{"class":284},[137,25704,23226],{"class":147},[137,25706,253],{"class":157},[137,25708,23231],{"class":284},[137,25710,4078],{"class":157},[137,25712,25713,25715,25717],{"class":139,"line":662},[137,25714,8374],{"class":157},[137,25716,25605],{"class":4036},[137,25718,4053],{"class":157},[137,25720,25721,25723,25725],{"class":139,"line":667},[137,25722,4072],{"class":157},[137,25724,4065],{"class":4036},[137,25726,4053],{"class":157},[137,25728,25729,25731,25733,25735,25737,25740,25742,25744],{"class":139,"line":786},[137,25730,9826],{"class":157},[137,25732,8330],{"class":4036},[137,25734,23757],{"class":147},[137,25736,253],{"class":157},[137,25738,25739],{"class":284},"\"navigation\"",[137,25741,4048],{"class":157},[137,25743,8330],{"class":4036},[137,25745,4053],{"class":157},[137,25747,25748,25750,25752,25754,25756,25759,25761,25763],{"class":139,"line":798},[137,25749,9826],{"class":157},[137,25751,8330],{"class":4036},[137,25753,23757],{"class":147},[137,25755,253],{"class":157},[137,25757,25758],{"class":284},"\"sign-in-iframe\"",[137,25760,4048],{"class":157},[137,25762,8330],{"class":4036},[137,25764,4053],{"class":157},[137,25766,25767,25769,25771,25773,25775,25778,25780,25782,25785,25787,25789],{"class":139,"line":803},[137,25768,9826],{"class":157},[137,25770,4037],{"class":4036},[137,25772,25639],{"class":147},[137,25774,253],{"class":157},[137,25776,25777],{"class":284},"\"module\"",[137,25779,4040],{"class":147},[137,25781,253],{"class":157},[137,25783,25784],{"class":284},"\"\u002Fsrc\u002Fmain.ts\"",[137,25786,4048],{"class":157},[137,25788,4037],{"class":4036},[137,25790,4053],{"class":157},[137,25792,25793,25795,25797],{"class":139,"line":931},[137,25794,8374],{"class":157},[137,25796,4065],{"class":4036},[137,25798,4053],{"class":157},[137,25800,25801,25803,25805],{"class":139,"line":1568},[137,25802,4083],{"class":157},[137,25804,4026],{"class":4036},[137,25806,4053],{"class":157},[27,25808,25809,25810,25812,25813,25815,25816,21802,25819,25821],{},"To create the navigation component, start by creating a ",[22,25811,21759],{}," dictionary inside the ",[22,25814,23035],{}," folder. Then, create a ",[22,25817,25818],{},"navigation.ts",[22,25820,21759],{}," dictionary.",[128,25823,25825],{"className":4024,"code":25824,"language":4026,"meta":133,"style":133},"export default function componentNavigation(userEmail = \"\", showSignIn = true) { return \u002F*html*\u002F `\n\u003Cnav class=\"bg-green-700\">\n    \u003Cdiv class=\"mx-auto max-w-7xl px-2 sm:px-6 lg:px-8\">\n        \u003Cdiv class=\"flex justify-between h-16 items-center\">\n            \u003Cdiv class=\"text-white rounded-md px-3 py-2 text-sm font-medium\">${userEmail}\u003C\u002Fdiv>\n            ${ showSignIn ? \u002F*html*\u002F `\u003Cbutton\n                id=\"sign-out-button\"\n                class=\" text-white hover:bg-green-500 hover:text-white rounded-md px-3 py-2 text-sm font-medium\"\n            >\n                Sign Out\u003C\u002Fbutton\n            >` : \"\" }\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Fnav>\n`; }\n",[22,25826,25827,25832,25848,25863,25878,25898,25905,25915,25925,25930,25937,25942,25950,25958,25966],{"__ignoreMap":133},[137,25828,25829],{"class":139,"line":140},[137,25830,25831],{"class":157},"export default function componentNavigation(userEmail = \"\", showSignIn = true) { return \u002F*html*\u002F `\n",[137,25833,25834,25836,25839,25841,25843,25846],{"class":139,"line":173},[137,25835,4033],{"class":157},[137,25837,25838],{"class":4036},"nav",[137,25840,7832],{"class":147},[137,25842,253],{"class":157},[137,25844,25845],{"class":284},"\"bg-green-700\"",[137,25847,4053],{"class":157},[137,25849,25850,25852,25854,25856,25858,25861],{"class":139,"line":188},[137,25851,4072],{"class":157},[137,25853,8330],{"class":4036},[137,25855,7832],{"class":147},[137,25857,253],{"class":157},[137,25859,25860],{"class":284},"\"mx-auto max-w-7xl px-2 sm:px-6 lg:px-8\"",[137,25862,4053],{"class":157},[137,25864,25865,25867,25869,25871,25873,25876],{"class":139,"line":269},[137,25866,9826],{"class":157},[137,25868,8330],{"class":4036},[137,25870,7832],{"class":147},[137,25872,253],{"class":157},[137,25874,25875],{"class":284},"\"flex justify-between h-16 items-center\"",[137,25877,4053],{"class":157},[137,25879,25880,25882,25884,25886,25888,25891,25894,25896],{"class":139,"line":278},[137,25881,23852],{"class":157},[137,25883,8330],{"class":4036},[137,25885,7832],{"class":147},[137,25887,253],{"class":157},[137,25889,25890],{"class":284},"\"text-white rounded-md px-3 py-2 text-sm font-medium\"",[137,25892,25893],{"class":157},">${userEmail}\u003C\u002F",[137,25895,8330],{"class":4036},[137,25897,4053],{"class":157},[137,25899,25900,25903],{"class":139,"line":291},[137,25901,25902],{"class":157},"            ${ showSignIn ? \u002F*html*\u002F `\u003C",[137,25904,8385],{"class":4036},[137,25906,25907,25910,25912],{"class":139,"line":297},[137,25908,25909],{"class":147},"                id",[137,25911,253],{"class":157},[137,25913,25914],{"class":284},"\"sign-out-button\"\n",[137,25916,25917,25920,25922],{"class":139,"line":302},[137,25918,25919],{"class":147},"                class",[137,25921,253],{"class":157},[137,25923,25924],{"class":284},"\" text-white hover:bg-green-500 hover:text-white rounded-md px-3 py-2 text-sm font-medium\"\n",[137,25926,25927],{"class":139,"line":662},[137,25928,25929],{"class":157},"            >\n",[137,25931,25932,25935],{"class":139,"line":667},[137,25933,25934],{"class":157},"                Sign Out\u003C\u002F",[137,25936,8385],{"class":4036},[137,25938,25939],{"class":139,"line":786},[137,25940,25941],{"class":157},"            >` : \"\" }\n",[137,25943,25944,25946,25948],{"class":139,"line":798},[137,25945,9843],{"class":157},[137,25947,8330],{"class":4036},[137,25949,4053],{"class":157},[137,25951,25952,25954,25956],{"class":139,"line":803},[137,25953,8374],{"class":157},[137,25955,8330],{"class":4036},[137,25957,4053],{"class":157},[137,25959,25960,25962,25964],{"class":139,"line":931},[137,25961,4083],{"class":157},[137,25963,25838],{"class":4036},[137,25965,4053],{"class":157},[137,25967,25968],{"class":139,"line":1568},[137,25969,24202],{"class":157},[27,25971,25972],{},"In the navigation component, we will display the user's email address and a Sign Out button if the currently authenticated user is logged in. If the user is not authenticated, we will not display anything and the navigation will be empty. Please refer to the images below.",[27,25974,25975],{},[63,25976],{"alt":25977,"src":25978},"Screenshot of the application with the login form","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1680414125\u002Fblog\u002Fsingle-sign-on-with-firebase-authentication-across-multiple-domains\u002Fsign-in-page_utf7pz",[27,25980,25981],{},[63,25982],{"alt":25983,"src":25984},"Screenshot of the application with the user is logged in","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1680414124\u002Fblog\u002Fsingle-sign-on-with-firebase-authentication-across-multiple-domains\u002Fsigned-in-user_uxusyr",[27,25986,25987,25988,25990],{},"Now, in the ",[22,25989,13114],{}," file, we are going to add the following code:",[128,25992,25994],{"className":13299,"code":25993,"language":13301,"meta":133,"style":133},"import componentNavigation from \".\u002Fcomponents\u002Fnavigation\";\n\nwindow.onmessage = function (e) {\n    setUserInfo(e.data);\n};\n\nconst navigation = document.querySelector(\"#navigation\") as HTMLDivElement;\nconst signInIframe = document.querySelector(\"#sign-in-iframe\") as HTMLIFrameElement;\n\nsetNavigationComponent();\nsetSignInIframe();\n\nfunction setUserInfo(userInfo: any) {\n    setNavigationComponent(userInfo);\n}\n\nfunction setNavigationComponent(userInfo: any = null) {\n    navigation.innerHTML = componentNavigation(userInfo?.email, userInfo === null ? false : true);\n    if (userInfo !== null) {\n        const signOutButton = document.querySelector(\"#sign-out-button\") as HTMLButtonElement;\n        signOutButton.addEventListener(\"click\", signOut);\n    }\n}\n\nfunction setSignInIframe() {\n    signInIframe.innerHTML = \u002F*html*\u002F `\u003Ciframe id=\"sign-in-form\" src=\"https:\u002F\u002Flocalhost:3000\" class=\"w-[100vw] h-[600px]\">\u003C\u002Fiframe>`;\n}\n\nfunction signOut() {\n    const iFrame = document.querySelector(\"#sign-in-form\") as HTMLIFrameElement;\n    if (iFrame.contentWindow) {\n        iFrame.contentWindow.postMessage(\"signOut\", \"https:\u002F\u002Flocalhost:3000\");\n    }\n}\n",[22,25995,25996,26010,26014,26030,26038,26042,26046,26073,26099,26103,26110,26117,26121,26140,26148,26152,26156,26177,26206,26220,26247,26262,26266,26270,26274,26283,26298,26302,26306,26314,26340,26347,26366,26370],{"__ignoreMap":133},[137,25997,25998,26000,26003,26005,26008],{"class":139,"line":140},[137,25999,10287],{"class":143},[137,26001,26002],{"class":157}," componentNavigation ",[137,26004,10954],{"class":143},[137,26006,26007],{"class":284}," \".\u002Fcomponents\u002Fnavigation\"",[137,26009,3276],{"class":157},[137,26011,26012],{"class":139,"line":173},[137,26013,516],{"emptyLinePlaceholder":515},[137,26015,26016,26018,26020,26022,26024,26026,26028],{"class":139,"line":188},[137,26017,24677],{"class":157},[137,26019,24680],{"class":147},[137,26021,151],{"class":143},[137,26023,154],{"class":143},[137,26025,158],{"class":157},[137,26027,24534],{"class":161},[137,26029,170],{"class":157},[137,26031,26032,26035],{"class":139,"line":269},[137,26033,26034],{"class":147},"    setUserInfo",[137,26036,26037],{"class":157},"(e.data);\n",[137,26039,26040],{"class":139,"line":278},[137,26041,191],{"class":157},[137,26043,26044],{"class":139,"line":291},[137,26045,516],{"emptyLinePlaceholder":515},[137,26047,26048,26050,26053,26055,26057,26059,26061,26064,26066,26068,26071],{"class":139,"line":297},[137,26049,3077],{"class":143},[137,26051,26052],{"class":364}," navigation",[137,26054,151],{"class":143},[137,26056,3717],{"class":157},[137,26058,3873],{"class":147},[137,26060,356],{"class":157},[137,26062,26063],{"class":284},"\"#navigation\"",[137,26065,219],{"class":157},[137,26067,24431],{"class":143},[137,26069,26070],{"class":147}," HTMLDivElement",[137,26072,3276],{"class":157},[137,26074,26075,26077,26080,26082,26084,26086,26088,26091,26093,26095,26097],{"class":139,"line":302},[137,26076,3077],{"class":143},[137,26078,26079],{"class":364}," signInIframe",[137,26081,151],{"class":143},[137,26083,3717],{"class":157},[137,26085,3873],{"class":147},[137,26087,356],{"class":157},[137,26089,26090],{"class":284},"\"#sign-in-iframe\"",[137,26092,219],{"class":157},[137,26094,24431],{"class":143},[137,26096,24434],{"class":147},[137,26098,3276],{"class":157},[137,26100,26101],{"class":139,"line":662},[137,26102,516],{"emptyLinePlaceholder":515},[137,26104,26105,26108],{"class":139,"line":667},[137,26106,26107],{"class":147},"setNavigationComponent",[137,26109,924],{"class":157},[137,26111,26112,26115],{"class":139,"line":786},[137,26113,26114],{"class":147},"setSignInIframe",[137,26116,924],{"class":157},[137,26118,26119],{"class":139,"line":798},[137,26120,516],{"emptyLinePlaceholder":515},[137,26122,26123,26125,26128,26130,26133,26135,26138],{"class":139,"line":803},[137,26124,483],{"class":143},[137,26126,26127],{"class":147}," setUserInfo",[137,26129,356],{"class":157},[137,26131,26132],{"class":161},"userInfo",[137,26134,894],{"class":143},[137,26136,26137],{"class":364}," any",[137,26139,170],{"class":157},[137,26141,26142,26145],{"class":139,"line":931},[137,26143,26144],{"class":147},"    setNavigationComponent",[137,26146,26147],{"class":157},"(userInfo);\n",[137,26149,26150],{"class":139,"line":1568},[137,26151,510],{"class":157},[137,26153,26154],{"class":139,"line":1573},[137,26155,516],{"emptyLinePlaceholder":515},[137,26157,26158,26160,26163,26165,26167,26169,26171,26173,26175],{"class":139,"line":1578},[137,26159,483],{"class":143},[137,26161,26162],{"class":147}," setNavigationComponent",[137,26164,356],{"class":157},[137,26166,26132],{"class":161},[137,26168,894],{"class":143},[137,26170,26137],{"class":364},[137,26172,151],{"class":143},[137,26174,3417],{"class":364},[137,26176,170],{"class":157},[137,26178,26179,26182,26184,26187,26190,26192,26194,26197,26199,26202,26204],{"class":139,"line":1588},[137,26180,26181],{"class":157},"    navigation.innerHTML ",[137,26183,253],{"class":143},[137,26185,26186],{"class":147}," componentNavigation",[137,26188,26189],{"class":157},"(userInfo?.email, userInfo ",[137,26191,5502],{"class":143},[137,26193,3417],{"class":364},[137,26195,26196],{"class":143}," ?",[137,26198,3387],{"class":364},[137,26200,26201],{"class":143}," :",[137,26203,14286],{"class":364},[137,26205,1502],{"class":157},[137,26207,26208,26210,26213,26216,26218],{"class":139,"line":1601},[137,26209,24696],{"class":143},[137,26211,26212],{"class":157}," (userInfo ",[137,26214,26215],{"class":143},"!==",[137,26217,3417],{"class":364},[137,26219,170],{"class":157},[137,26221,26222,26224,26227,26229,26231,26233,26235,26238,26240,26242,26245],{"class":139,"line":3802},[137,26223,3008],{"class":143},[137,26225,26226],{"class":364}," signOutButton",[137,26228,151],{"class":143},[137,26230,3717],{"class":157},[137,26232,3873],{"class":147},[137,26234,356],{"class":157},[137,26236,26237],{"class":284},"\"#sign-out-button\"",[137,26239,219],{"class":157},[137,26241,24431],{"class":143},[137,26243,26244],{"class":147}," HTMLButtonElement",[137,26246,3276],{"class":157},[137,26248,26249,26252,26254,26256,26259],{"class":139,"line":3808},[137,26250,26251],{"class":157},"        signOutButton.",[137,26253,4412],{"class":147},[137,26255,356],{"class":157},[137,26257,26258],{"class":284},"\"click\"",[137,26260,26261],{"class":157},", signOut);\n",[137,26263,26264],{"class":139,"line":3822},[137,26265,294],{"class":157},[137,26267,26268],{"class":139,"line":3827},[137,26269,510],{"class":157},[137,26271,26272],{"class":139,"line":3832},[137,26273,516],{"emptyLinePlaceholder":515},[137,26275,26276,26278,26281],{"class":139,"line":3840},[137,26277,483],{"class":143},[137,26279,26280],{"class":147}," setSignInIframe",[137,26282,275],{"class":157},[137,26284,26285,26288,26290,26293,26296],{"class":139,"line":3846},[137,26286,26287],{"class":157},"    signInIframe.innerHTML ",[137,26289,253],{"class":143},[137,26291,26292],{"class":308}," \u002F*html*\u002F",[137,26294,26295],{"class":284}," `\u003Ciframe id=\"sign-in-form\" src=\"https:\u002F\u002Flocalhost:3000\" class=\"w-[100vw] h-[600px]\">\u003C\u002Fiframe>`",[137,26297,3276],{"class":157},[137,26299,26300],{"class":139,"line":3861},[137,26301,510],{"class":157},[137,26303,26304],{"class":139,"line":3883},[137,26305,516],{"emptyLinePlaceholder":515},[137,26307,26308,26310,26312],{"class":139,"line":3896},[137,26309,483],{"class":143},[137,26311,25174],{"class":147},[137,26313,275],{"class":157},[137,26315,26316,26318,26321,26323,26325,26327,26329,26332,26334,26336,26338],{"class":139,"line":3901},[137,26317,4177],{"class":143},[137,26319,26320],{"class":364}," iFrame",[137,26322,151],{"class":143},[137,26324,3717],{"class":157},[137,26326,3873],{"class":147},[137,26328,356],{"class":157},[137,26330,26331],{"class":284},"\"#sign-in-form\"",[137,26333,219],{"class":157},[137,26335,24431],{"class":143},[137,26337,24434],{"class":147},[137,26339,3276],{"class":157},[137,26341,26342,26344],{"class":139,"line":3906},[137,26343,24696],{"class":143},[137,26345,26346],{"class":157}," (iFrame.contentWindow) {\n",[137,26348,26349,26352,26354,26356,26359,26361,26364],{"class":139,"line":3911},[137,26350,26351],{"class":157},"        iFrame.contentWindow.",[137,26353,24799],{"class":147},[137,26355,356],{"class":157},[137,26357,26358],{"class":284},"\"signOut\"",[137,26360,164],{"class":157},[137,26362,26363],{"class":284},"\"https:\u002F\u002Flocalhost:3000\"",[137,26365,1502],{"class":157},[137,26367,26368],{"class":139,"line":4666},[137,26369,294],{"class":157},[137,26371,26372],{"class":139,"line":4672},[137,26373,510],{"class":157},[27,26375,26376,26377,26379,26380,26382],{},"From the code in the ",[22,26378,13114],{}," file above, we can see that we listen for events from the iframe by adding the ",[22,26381,25258],{}," function. When we receive user info, we re-render the navigation component.",[128,26384,26386],{"className":13299,"code":26385,"language":13301,"meta":133,"style":133},"window.onmessage = function (e) {\n    setUserInfo(e.data);\n};\n",[22,26387,26388,26404,26410],{"__ignoreMap":133},[137,26389,26390,26392,26394,26396,26398,26400,26402],{"class":139,"line":140},[137,26391,24677],{"class":157},[137,26393,24680],{"class":147},[137,26395,151],{"class":143},[137,26397,154],{"class":143},[137,26399,158],{"class":157},[137,26401,24534],{"class":161},[137,26403,170],{"class":157},[137,26405,26406,26408],{"class":139,"line":173},[137,26407,26034],{"class":147},[137,26409,26037],{"class":157},[137,26411,26412],{"class":139,"line":188},[137,26413,191],{"class":157},[27,26415,26416,26417,26419],{},"To handle sign out, we send a ",[22,26418,24799],{}," to the iFrame. In the previous section, we created a sign out method to handle this type of situation.",[128,26421,26423],{"className":13299,"code":26422,"language":13301,"meta":133,"style":133},"function signOut() {\n    const iFrame = document.querySelector(\"#sign-in-form\") as HTMLIFrameElement;\n    if (iFrame.contentWindow) {\n        iFrame.contentWindow.postMessage(\"signOut\", \"https:\u002F\u002Flocalhost:3000\");\n    }\n}\n",[22,26424,26425,26433,26457,26463,26479,26483],{"__ignoreMap":133},[137,26426,26427,26429,26431],{"class":139,"line":140},[137,26428,483],{"class":143},[137,26430,25174],{"class":147},[137,26432,275],{"class":157},[137,26434,26435,26437,26439,26441,26443,26445,26447,26449,26451,26453,26455],{"class":139,"line":173},[137,26436,4177],{"class":143},[137,26438,26320],{"class":364},[137,26440,151],{"class":143},[137,26442,3717],{"class":157},[137,26444,3873],{"class":147},[137,26446,356],{"class":157},[137,26448,26331],{"class":284},[137,26450,219],{"class":157},[137,26452,24431],{"class":143},[137,26454,24434],{"class":147},[137,26456,3276],{"class":157},[137,26458,26459,26461],{"class":139,"line":188},[137,26460,24696],{"class":143},[137,26462,26346],{"class":157},[137,26464,26465,26467,26469,26471,26473,26475,26477],{"class":139,"line":269},[137,26466,26351],{"class":157},[137,26468,24799],{"class":147},[137,26470,356],{"class":157},[137,26472,26358],{"class":284},[137,26474,164],{"class":157},[137,26476,26363],{"class":284},[137,26478,1502],{"class":157},[137,26480,26481],{"class":139,"line":278},[137,26482,294],{"class":157},[137,26484,26485],{"class":139,"line":291},[137,26486,510],{"class":157},[27,26488,26489,26490,26493],{},"We have now completed the implementation inside the iFrame. The next step is to implement another identical iFrame Vite project that will run on ",[22,26491,26492],{},"https:\u002F\u002Flocalhost:3002",". Since we have already covered this step, we will not repeat it in this section. Please create the project yourself.",[27,26495,26496,26497,1017],{},"Now, if you run all three projects at the same time (Authentication, iFrame 1, and iFrame 2), you will see that authentication works seamlessly between all the projects. This marks the end of this blog article. We hope you found it useful. You can find the GitHub repository of the project at the following ",[45,26498,2726],{"href":26499,"target":2716,"rel":26500},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fsingle-sign-on-with-firebase",[2718,2719],[104,26502,2567],{"id":2566},[27,26504,26505],{},"Firebase Authentication is a widely used authentication service for web applications. However, Firebase has a limitation that prevents users from remaining signed in once they have signed in across multiple domains. In this blog article, we will discuss how to work around this limitation and enable Single Sign-On (SSO) with Firebase Authentication across multiple domains.",[27,26507,26508,26509,3596,26511,1017],{},"To enable SSO with Firebase Authentication across multiple domains, we created an authentication project where we implemented all the logic to handle authentication. We then embedded the authentication project in two other applications hosted on different domains using an iframe. To communicate between the authentication project and the other two projects, we used the ",[22,26510,24799],{},[45,26512,26515],{"href":26513,"target":2716,"rel":26514},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWindow\u002FpostMessage",[2718,2719],"API",[27,26517,26518],{},"By the end of the blog article, we were able to seamlessly authenticate users across all domains, enabling them to sign in once and remain signed in across all applications.",[2617,26520,26521],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":133,"searchDepth":173,"depth":173,"links":26523},[26524,26525,26526,26527,26528,26529],{"id":22773,"depth":173,"text":22774},{"id":23042,"depth":173,"text":23043},{"id":23268,"depth":173,"text":23269},{"id":23727,"depth":173,"text":23728},{"id":25535,"depth":173,"text":25536},{"id":2566,"depth":173,"text":2567},"Firebase is a great platform that offers a wide range of services to developers, making it easy to build, improve, and grow their apps. One of these services is Firebase Auth, which allows for easy user authentication using its JavaScript SDK. Despite Firebase Auth being an awesome service and abstracting a lot of the complex code needed to build user authentication, I recently found one limitation. Firebase Auth doesn't persist the session across multiple domains. This means that if we use the same Firebase Auth for two different domains, such as https:\u002F\u002Fexample1.com and https:\u002F\u002Fexample2.com, we have to sign in to both applications on both domains independently. Currently, Firebase doesn't have a built-in feature to handle this situation out of the box.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1680413367\u002Fblog\u002Fsingle-sign-on-with-firebase-authentication-across-multiple-domains\u002FSSO-with-firebase-authentication-across-multiple-domains_nil2ba",[22705,2668,22693,26533,3537,26534,26535,26536,26537],"SSO","Workaround","iframe","persist session","multiple domains",{},"\u002F2023\u002F04\u002F02\u002Fsingle-sign-on-with-firebase-authentication-across-multiple-domains","2nd April 2023",{"title":22705,"description":26530},"2023\u002F04\u002F02\u002Fsingle-sign-on-with-firebase-authentication-across-multiple-domains","1fxIz0sY5-7BgHBeHawvr_jrYZ_JhiZHSSBuwxVIj_I",{"id":26545,"title":26546,"articleTags":26547,"author":11,"blog":12,"body":26548,"description":27867,"extension":2649,"image":27868,"keywords":27869,"meta":27876,"navigation":515,"path":27877,"published":27878,"readTime":140,"seo":27879,"stem":27880,"type":2662,"__hash__":27881},"content\u002F2023\u002F04\u002F25\u002Fsignals-in-vanilla-js.md","Signals in Vanilla JS",[9,12817,22707],{"type":14,"value":26549,"toc":27862},[26550,26553,26567,26569,26573,26578,26607,26610,26613,26617,26620,26632,26635,26647,26650,26690,26712,26715,26732,26746,26758,26764,26847,26850,26861,26865,26868,26882,26887,27284,27299,27304,27325,27332,27365,27377,27404,27413,27417,27420,27434,27439,27810,27823,27846,27852,27859],[17,26551,26546],{"id":26552},"signals-in-vanilla-js",[27,26554,26555],{},[30,26556,26557,36,26559,40,26561],{},[33,26558],{"value":35},[33,26560],{"value":39},[42,26562,26563],{},[45,26564,26565],{"href":47},[33,26566],{"value":50},[52,26568],{":tags":54},[56,26570],{":audio-src":26571,":transcript-src":26572},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2023\u002F04\u002F25\u002Fsignals-in-vanilla-js\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2023\u002F04\u002F25\u002Fsignals-in-vanilla-js\u002Fsummary.json",[27,26574,26575],{},[63,26576],{"alt":12847,"src":26577},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1682415127\u002Fblog\u002Fsignals-in-vanilla-js\u002Fsignals-in-vanilla-js-hero_lsdah8",[27,26579,26580,26581,164,26586,164,26591,164,26596,14528,26601,26606],{},"If you're a JavaScript developer, you may have heard about the buzz around signals lately. Several popular JavaScript frameworks, such as ",[45,26582,26585],{"href":26583,"target":2716,"rel":26584},"https:\u002F\u002Fvuejs.org\u002Fguide\u002Fextras\u002Freactivity-in-depth.html#connection-to-signals",[2718,2719],"Vue",[45,26587,26590],{"href":26588,"target":2716,"rel":26589},"https:\u002F\u002Fpreactjs.com\u002Fguide\u002Fv10\u002Fsignals\u002F",[2718,2719],"Preact",[45,26592,26595],{"href":26593,"target":2716,"rel":26594},"https:\u002F\u002Fwww.solidjs.com\u002Fdocs\u002Flatest\u002Fapi#createsignal",[2718,2719],"Solid",[45,26597,26600],{"href":26598,"target":2716,"rel":26599},"https:\u002F\u002Fgithub.com\u002Fangular\u002Fangular\u002Fdiscussions\u002F49090",[2718,2719],"Angular",[45,26602,26605],{"href":26603,"target":2716,"rel":26604},"https:\u002F\u002Fqwik.builder.io\u002Fdocs\u002Fcomponents\u002Fstate\u002F#usesignal",[2718,2719],"Quick",", have recently implemented and supported the use of signals. But what exactly are signals?",[27,26608,26609],{},"As web applications become more complex, managing the state of JavaScript applications becomes a challenge. This is where signals can be a valuable tool, allowing the application to respond to changes in the state, such as a variable change.",[27,26611,26612],{},"In this article, we will explore the concept of signals and how they can be used in Vanilla JavaScript applications. We will use the Vue and Preact reactivity libraries and see how easy and similar these two approaches are to implement. For the demo project, we will use Vite. So let's get started by creating a Vite app.",[104,26614,26616],{"id":26615},"create-a-vite-app","Create a Vite app",[27,26618,26619],{},"To create a brand new Vite app, run the following command in your terminal:",[128,26621,26622],{"className":8665,"code":22783,"language":8667,"meta":133,"style":133},[22,26623,26624],{"__ignoreMap":133},[137,26625,26626,26628,26630],{"class":139,"line":140},[137,26627,17263],{"class":147},[137,26629,22792],{"class":284},[137,26631,22795],{"class":284},[27,26633,26634],{},"Name the project \"signals-in-vanilla-js\"",[128,26636,26638],{"className":8665,"code":26637,"language":8667,"meta":133,"style":133},"? Project name: › signals-in-vanilla-js\n",[22,26639,26640],{"__ignoreMap":133},[137,26641,26642,26644],{"class":139,"line":140},[137,26643,12972],{"class":143},[137,26645,26646],{"class":157}," Project name: › signals-in-vanilla-js\n",[27,26648,26649],{},"Select Vanilla with TypeScript.",[128,26651,26652],{"className":8665,"code":22810,"language":8667,"meta":133,"style":133},[22,26653,26654,26660,26666,26670,26674,26678,26682,26686],{"__ignoreMap":133},[137,26655,26656,26658],{"class":139,"line":140},[137,26657,12972],{"class":143},[137,26659,22819],{"class":157},[137,26661,26662,26664],{"class":139,"line":173},[137,26663,12983],{"class":147},[137,26665,22826],{"class":284},[137,26667,26668],{"class":139,"line":188},[137,26669,22831],{"class":147},[137,26671,26672],{"class":139,"line":269},[137,26673,22836],{"class":147},[137,26675,26676],{"class":139,"line":278},[137,26677,22841],{"class":147},[137,26679,26680],{"class":139,"line":291},[137,26681,22846],{"class":147},[137,26683,26684],{"class":139,"line":297},[137,26685,22851],{"class":147},[137,26687,26688],{"class":139,"line":302},[137,26689,22856],{"class":147},[128,26691,26693],{"className":8665,"code":26692,"language":8667,"meta":133,"style":133},"? Select a variant: › - Use arrow-keys. Return to submit.\n❯   TypeScript\n    JavaScript\n",[22,26694,26695,26701,26707],{"__ignoreMap":133},[137,26696,26697,26699],{"class":139,"line":140},[137,26698,12972],{"class":143},[137,26700,22868],{"class":157},[137,26702,26703,26705],{"class":139,"line":173},[137,26704,12983],{"class":147},[137,26706,22880],{"class":284},[137,26708,26709],{"class":139,"line":188},[137,26710,26711],{"class":147},"    JavaScript\n",[27,26713,26714],{},"After creating the new project, navigate to the root directory of the project and install the dependencies using the following commands in the terminal:",[128,26716,26718],{"className":8665,"code":26717,"language":8667,"meta":133,"style":133},"cd signals-in-vanilla-js\nyarn\n",[22,26719,26720,26727],{"__ignoreMap":133},[137,26721,26722,26724],{"class":139,"line":140},[137,26723,9558],{"class":364},[137,26725,26726],{"class":284}," signals-in-vanilla-js\n",[137,26728,26729],{"class":139,"line":173},[137,26730,26731],{"class":147},"yarn\n",[27,26733,26734,26735,26738,26739,26741,26742,26745],{},"By default, Vite runs on port ",[22,26736,26737],{},"5173",". This is not a big deal, but you can easily change it to run on port ",[22,26740,15111],{}," with ",[22,26743,26744],{},"https"," by installing the following package:",[128,26747,26748],{"className":8665,"code":22925,"language":8667,"meta":133,"style":133},[22,26749,26750],{"__ignoreMap":133},[137,26751,26752,26754,26756],{"class":139,"line":140},[137,26753,17263],{"class":147},[137,26755,17266],{"class":284},[137,26757,22936],{"class":284},[27,26759,26760,26761,26763],{},"To begin, create a file called ",[22,26762,22068],{}," in the root of your project and add the following code:",[128,26765,26766],{"className":8665,"code":22944,"language":8667,"meta":133,"style":133},[22,26767,26768,26784,26797,26801,26807,26814,26822,26831,26835,26843],{"__ignoreMap":133},[137,26769,26770,26772,26774,26776,26778,26780,26782],{"class":139,"line":140},[137,26771,10287],{"class":147},[137,26773,22084],{"class":284},[137,26775,22087],{"class":284},[137,26777,22090],{"class":284},[137,26779,10293],{"class":284},[137,26781,22095],{"class":284},[137,26783,3276],{"class":157},[137,26785,26786,26788,26791,26793,26795],{"class":139,"line":173},[137,26787,10287],{"class":147},[137,26789,26790],{"class":284}," basicSsl",[137,26792,10293],{"class":284},[137,26794,22971],{"class":284},[137,26796,3276],{"class":157},[137,26798,26799],{"class":139,"line":188},[137,26800,516],{"emptyLinePlaceholder":515},[137,26802,26803,26805],{"class":139,"line":269},[137,26804,13456],{"class":143},[137,26806,22108],{"class":157},[137,26808,26809,26812],{"class":139,"line":278},[137,26810,26811],{"class":147},"    server:",[137,26813,256],{"class":284},[137,26815,26816,26819],{"class":139,"line":291},[137,26817,26818],{"class":147},"        port:",[137,26820,26821],{"class":284}," 3000,\n",[137,26823,26824,26827,26829],{"class":139,"line":297},[137,26825,26826],{"class":147},"        https:",[137,26828,14286],{"class":364},[137,26830,1961],{"class":284},[137,26832,26833],{"class":139,"line":302},[137,26834,775],{"class":157},[137,26836,26837,26840],{"class":139,"line":662},[137,26838,26839],{"class":147},"    plugins:",[137,26841,26842],{"class":157}," [basicSsl()],\n",[137,26844,26845],{"class":139,"line":667},[137,26846,5422],{"class":157},[27,26848,26849],{},"Now, we can run our project in the browser.",[128,26851,26853],{"className":8665,"code":26852,"language":8667,"meta":133,"style":133},"yarn dev\n",[22,26854,26855],{"__ignoreMap":133},[137,26856,26857,26859],{"class":139,"line":140},[137,26858,17263],{"class":147},[137,26860,9581],{"class":284},[104,26862,26864],{"id":26863},"using-signals-with-preact","Using Signals with Preact",[27,26866,26867],{},"To use Signals, we need to install the Preact package. Run the following command in the terminal:",[128,26869,26871],{"className":8665,"code":26870,"language":8667,"meta":133,"style":133},"yarn add @preact\u002Fsignals-core\n",[22,26872,26873],{"__ignoreMap":133},[137,26874,26875,26877,26879],{"class":139,"line":140},[137,26876,17263],{"class":147},[137,26878,17266],{"class":284},[137,26880,26881],{"class":284}," @preact\u002Fsignals-core\n",[27,26883,26884,26885,9772],{},"Next, we need to add the following code to the ",[22,26886,13114],{},[128,26888,26890],{"className":13299,"code":26889,"language":13301,"meta":133,"style":133},"import \".\u002Fstyle.css\";\nimport { signal, computed, effect } from \"@preact\u002Fsignals-core\";\nconst counter = signal(1);\nconst result = computed(() => counter.value * 10);\n\ndocument.querySelector\u003CHTMLDivElement>(\"#app\")!.innerHTML = `\n  \u003Cdiv>\n    \u003Ch1>Signals in Vanilla JS\u003C\u002Fh1>\n    \u003Cdiv class=\"card\">\n      \u003Cbutton type=\"button\">count is \u003Cspan class=\"counter\">${counter.value}\u003C\u002Fspan>\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n    \u003Cp>Result: \u003Cspan class=\"counter\">${counter.value}\u003C\u002Fspan> * 10 = \u003Cspan class=\"result\">10\u003C\u002Fspan>\u003C\u002Fp>\n  \u003C\u002Fdiv>\n`;\n\nfunction setCounter() {\n    const counterElements = document.querySelectorAll\u003CHTMLSpanElement>(\".counter\");\n    const resultElement = document.querySelector\u003CHTMLSpanElement>(\".result\");\n\n    if (!counterElements?.length || !resultElement) {\n        return;\n    }\n\n    counterElements.forEach((element) => {\n        element.innerText = counter.value.toString();\n    });\n\n    resultElement.innerText = result.value.toString();\n}\n\nconst button = document.querySelector\u003CHTMLButtonElement>(\"button\");\n\nbutton?.addEventListener(\"click\", () => {\n    counter.value++;\n});\n\neffect(setCounter);\n",[22,26891,26892,26901,26915,26933,26958,26962,26990,26995,27000,27005,27020,27024,27038,27043,27049,27053,27062,27088,27112,27116,27137,27143,27147,27151,27169,27183,27187,27191,27205,27209,27213,27238,27242,27259,27268,27272,27276],{"__ignoreMap":133},[137,26893,26894,26896,26899],{"class":139,"line":140},[137,26895,10287],{"class":143},[137,26897,26898],{"class":284}," \".\u002Fstyle.css\"",[137,26900,3276],{"class":157},[137,26902,26903,26905,26908,26910,26913],{"class":139,"line":173},[137,26904,10287],{"class":143},[137,26906,26907],{"class":157}," { signal, computed, effect } ",[137,26909,10954],{"class":143},[137,26911,26912],{"class":284}," \"@preact\u002Fsignals-core\"",[137,26914,3276],{"class":157},[137,26916,26917,26919,26922,26924,26927,26929,26931],{"class":139,"line":188},[137,26918,3077],{"class":143},[137,26920,26921],{"class":364}," counter",[137,26923,151],{"class":143},[137,26925,26926],{"class":147}," signal",[137,26928,356],{"class":157},[137,26930,6065],{"class":364},[137,26932,1502],{"class":157},[137,26934,26935,26937,26940,26942,26945,26947,26949,26952,26954,26956],{"class":139,"line":269},[137,26936,3077],{"class":143},[137,26938,26939],{"class":364}," result",[137,26941,151],{"class":143},[137,26943,26944],{"class":147}," computed",[137,26946,3193],{"class":157},[137,26948,222],{"class":143},[137,26950,26951],{"class":157}," counter.value ",[137,26953,7672],{"class":143},[137,26955,8023],{"class":364},[137,26957,1502],{"class":157},[137,26959,26960],{"class":139,"line":278},[137,26961,516],{"emptyLinePlaceholder":515},[137,26963,26964,26967,26969,26971,26974,26977,26979,26981,26983,26986,26988],{"class":139,"line":291},[137,26965,26966],{"class":157},"document.",[137,26968,3873],{"class":147},[137,26970,4033],{"class":157},[137,26972,26973],{"class":147},"HTMLDivElement",[137,26975,26976],{"class":157},">(",[137,26978,4514],{"class":284},[137,26980,14105],{"class":157},[137,26982,17393],{"class":143},[137,26984,26985],{"class":157},".innerHTML ",[137,26987,253],{"class":143},[137,26989,3778],{"class":284},[137,26991,26992],{"class":139,"line":297},[137,26993,26994],{"class":284},"  \u003Cdiv>\n",[137,26996,26997],{"class":139,"line":302},[137,26998,26999],{"class":284},"    \u003Ch1>Signals in Vanilla JS\u003C\u002Fh1>\n",[137,27001,27002],{"class":139,"line":662},[137,27003,27004],{"class":284},"    \u003Cdiv class=\"card\">\n",[137,27006,27007,27010,27013,27015,27017],{"class":139,"line":667},[137,27008,27009],{"class":284},"      \u003Cbutton type=\"button\">count is \u003Cspan class=\"counter\">${",[137,27011,27012],{"class":157},"counter",[137,27014,1017],{"class":284},[137,27016,5414],{"class":157},[137,27018,27019],{"class":284},"}\u003C\u002Fspan>\u003C\u002Fbutton>\n",[137,27021,27022],{"class":139,"line":786},[137,27023,21366],{"class":284},[137,27025,27026,27029,27031,27033,27035],{"class":139,"line":798},[137,27027,27028],{"class":284},"    \u003Cp>Result: \u003Cspan class=\"counter\">${",[137,27030,27012],{"class":157},[137,27032,1017],{"class":284},[137,27034,5414],{"class":157},[137,27036,27037],{"class":284},"}\u003C\u002Fspan> * 10 = \u003Cspan class=\"result\">10\u003C\u002Fspan>\u003C\u002Fp>\n",[137,27039,27040],{"class":139,"line":803},[137,27041,27042],{"class":284},"  \u003C\u002Fdiv>\n",[137,27044,27045,27047],{"class":139,"line":931},[137,27046,22056],{"class":284},[137,27048,3276],{"class":157},[137,27050,27051],{"class":139,"line":1568},[137,27052,516],{"emptyLinePlaceholder":515},[137,27054,27055,27057,27060],{"class":139,"line":1573},[137,27056,483],{"class":143},[137,27058,27059],{"class":147}," setCounter",[137,27061,275],{"class":157},[137,27063,27064,27066,27069,27071,27073,27076,27078,27081,27083,27086],{"class":139,"line":1578},[137,27065,4177],{"class":143},[137,27067,27068],{"class":364}," counterElements",[137,27070,151],{"class":143},[137,27072,3717],{"class":157},[137,27074,27075],{"class":147},"querySelectorAll",[137,27077,4033],{"class":157},[137,27079,27080],{"class":147},"HTMLSpanElement",[137,27082,26976],{"class":157},[137,27084,27085],{"class":284},"\".counter\"",[137,27087,1502],{"class":157},[137,27089,27090,27092,27095,27097,27099,27101,27103,27105,27107,27110],{"class":139,"line":1588},[137,27091,4177],{"class":143},[137,27093,27094],{"class":364}," resultElement",[137,27096,151],{"class":143},[137,27098,3717],{"class":157},[137,27100,3873],{"class":147},[137,27102,4033],{"class":157},[137,27104,27080],{"class":147},[137,27106,26976],{"class":157},[137,27108,27109],{"class":284},"\".result\"",[137,27111,1502],{"class":157},[137,27113,27114],{"class":139,"line":1601},[137,27115,516],{"emptyLinePlaceholder":515},[137,27117,27118,27120,27122,27124,27127,27129,27131,27134],{"class":139,"line":3802},[137,27119,24696],{"class":143},[137,27121,158],{"class":157},[137,27123,17393],{"class":143},[137,27125,27126],{"class":157},"counterElements?.",[137,27128,8611],{"class":364},[137,27130,24707],{"class":143},[137,27132,27133],{"class":143}," !",[137,27135,27136],{"class":157},"resultElement) {\n",[137,27138,27139,27141],{"class":139,"line":3808},[137,27140,5472],{"class":143},[137,27142,3276],{"class":157},[137,27144,27145],{"class":139,"line":3822},[137,27146,294],{"class":157},[137,27148,27149],{"class":139,"line":3827},[137,27150,516],{"emptyLinePlaceholder":515},[137,27152,27153,27156,27158,27160,27163,27165,27167],{"class":139,"line":3832},[137,27154,27155],{"class":157},"    counterElements.",[137,27157,8564],{"class":147},[137,27159,2774],{"class":157},[137,27161,27162],{"class":161},"element",[137,27164,219],{"class":157},[137,27166,222],{"class":143},[137,27168,256],{"class":157},[137,27170,27171,27174,27176,27179,27181],{"class":139,"line":3840},[137,27172,27173],{"class":157},"        element.innerText ",[137,27175,253],{"class":143},[137,27177,27178],{"class":157}," counter.value.",[137,27180,7692],{"class":147},[137,27182,924],{"class":157},[137,27184,27185],{"class":139,"line":3846},[137,27186,2832],{"class":157},[137,27188,27189],{"class":139,"line":3861},[137,27190,516],{"emptyLinePlaceholder":515},[137,27192,27193,27196,27198,27201,27203],{"class":139,"line":3883},[137,27194,27195],{"class":157},"    resultElement.innerText ",[137,27197,253],{"class":143},[137,27199,27200],{"class":157}," result.value.",[137,27202,7692],{"class":147},[137,27204,924],{"class":157},[137,27206,27207],{"class":139,"line":3896},[137,27208,510],{"class":157},[137,27210,27211],{"class":139,"line":3901},[137,27212,516],{"emptyLinePlaceholder":515},[137,27214,27215,27217,27220,27222,27224,27226,27228,27231,27233,27236],{"class":139,"line":3906},[137,27216,3077],{"class":143},[137,27218,27219],{"class":364}," button",[137,27221,151],{"class":143},[137,27223,3717],{"class":157},[137,27225,3873],{"class":147},[137,27227,4033],{"class":157},[137,27229,27230],{"class":147},"HTMLButtonElement",[137,27232,26976],{"class":157},[137,27234,27235],{"class":284},"\"button\"",[137,27237,1502],{"class":157},[137,27239,27240],{"class":139,"line":3911},[137,27241,516],{"emptyLinePlaceholder":515},[137,27243,27244,27247,27249,27251,27253,27255,27257],{"class":139,"line":4666},[137,27245,27246],{"class":157},"button?.",[137,27248,4412],{"class":147},[137,27250,356],{"class":157},[137,27252,26258],{"class":284},[137,27254,4420],{"class":157},[137,27256,222],{"class":143},[137,27258,256],{"class":157},[137,27260,27261,27264,27266],{"class":139,"line":4672},[137,27262,27263],{"class":157},"    counter.value",[137,27265,12683],{"class":143},[137,27267,3276],{"class":157},[137,27269,27270],{"class":139,"line":4680},[137,27271,5422],{"class":157},[137,27273,27274],{"class":139,"line":4711},[137,27275,516],{"emptyLinePlaceholder":515},[137,27277,27278,27281],{"class":139,"line":4716},[137,27279,27280],{"class":147},"effect",[137,27282,27283],{"class":157},"(setCounter);\n",[27,27285,27286,27287,164,27290,14528,27293,20514,27295,27298],{},"Let's explain what the code above does. First, we import ",[22,27288,27289],{},"signal",[22,27291,27292],{},"computed",[22,27294,27280],{},[22,27296,27297],{},"@preact\u002Fsignals-core",", which we have installed.",[27,27300,27301,27302,1017],{},"We can easily define reactive variables using ",[22,27303,27289],{},[128,27305,27307],{"className":13299,"code":27306,"language":13301,"meta":133,"style":133},"const counter = signal(1);\n",[22,27308,27309],{"__ignoreMap":133},[137,27310,27311,27313,27315,27317,27319,27321,27323],{"class":139,"line":140},[137,27312,3077],{"class":143},[137,27314,26921],{"class":364},[137,27316,151],{"class":143},[137,27318,26926],{"class":147},[137,27320,356],{"class":157},[137,27322,6065],{"class":364},[137,27324,1502],{"class":157},[27,27326,27327,27328,27331],{},"To update the counter signal, simply access the ",[22,27329,27330],{},".value"," property and update it with the desired value. In this case, the button click event increases the counter signal by one.",[128,27333,27335],{"className":13299,"code":27334,"language":13301,"meta":133,"style":133},"button?.addEventListener(\"click\", () => {\n    counter.value++;\n});\n",[22,27336,27337,27353,27361],{"__ignoreMap":133},[137,27338,27339,27341,27343,27345,27347,27349,27351],{"class":139,"line":140},[137,27340,27246],{"class":157},[137,27342,4412],{"class":147},[137,27344,356],{"class":157},[137,27346,26258],{"class":284},[137,27348,4420],{"class":157},[137,27350,222],{"class":143},[137,27352,256],{"class":157},[137,27354,27355,27357,27359],{"class":139,"line":173},[137,27356,27263],{"class":157},[137,27358,12683],{"class":143},[137,27360,3276],{"class":157},[137,27362,27363],{"class":139,"line":188},[137,27364,5422],{"class":157},[27,27366,27367,27368,27370,27371,6979,27374,27376],{},"The Preact library provides a ",[22,27369,27292],{}," function that allows us to compute a reactive value based on a signal. In our case, we multiply the counter by 10 in the computed value called ",[22,27372,27373],{},"result",[22,27375,27373],{}," returns the newly calculated value whenever the counter signal changes.",[128,27378,27380],{"className":13299,"code":27379,"language":13301,"meta":133,"style":133},"const result = computed(() => counter.value * 10);\n",[22,27381,27382],{"__ignoreMap":133},[137,27383,27384,27386,27388,27390,27392,27394,27396,27398,27400,27402],{"class":139,"line":140},[137,27385,3077],{"class":143},[137,27387,26939],{"class":364},[137,27389,151],{"class":143},[137,27391,26944],{"class":147},[137,27393,3193],{"class":157},[137,27395,222],{"class":143},[137,27397,26951],{"class":157},[137,27399,7672],{"class":143},[137,27401,8023],{"class":364},[137,27403,1502],{"class":157},[27,27405,27406,27407,27409,27410,27412],{},"Finally, we use the ",[22,27408,27280],{}," function to watch for any signal changes inside a function passed as a parameter. ",[22,27411,27280],{}," will execute the function anytime there is a signal change. In this way, we can re-render the counter and result to the HTML any time we update the counter signal.",[104,27414,27416],{"id":27415},"using-signals-with-vue","Using Signals with Vue",[27,27418,27419],{},"Just as we implemented signals using the Preact library, we can do the same with Vue. Let's install Vue by running the following command in the terminal:",[128,27421,27423],{"className":8665,"code":27422,"language":8667,"meta":133,"style":133},"yarn add vue\n",[22,27424,27425],{"__ignoreMap":133},[137,27426,27427,27429,27431],{"class":139,"line":140},[137,27428,17263],{"class":147},[137,27430,17266],{"class":284},[137,27432,27433],{"class":284}," vue\n",[27,27435,27436,27437,10277],{},"Next, replace the following code inside the ",[22,27438,13114],{},[128,27440,27442],{"className":13299,"code":27441,"language":13301,"meta":133,"style":133},"import \".\u002Fstyle.css\";\nimport { ref, computed, watch } from \"vue\";\nconst counter = ref(1);\nconst result = computed(() => counter.value * 10);\n\ndocument.querySelector\u003CHTMLDivElement>(\"#app\")!.innerHTML = `\n  \u003Cdiv>\n    \u003Ch1>Signals in Vanilla JS\u003C\u002Fh1>\n    \u003Cdiv class=\"card\">\n      \u003Cbutton type=\"button\">count is \u003Cspan class=\"counter\">${counter.value}\u003C\u002Fspan>\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n    \u003Cp>Result: \u003Cspan class=\"counter\">${counter.value}\u003C\u002Fspan> * 10 = \u003Cspan class=\"result\">10\u003C\u002Fspan>\u003C\u002Fp>\n  \u003C\u002Fdiv>\n`;\n\nfunction setCounter() {\n    const counterElements = document.querySelectorAll\u003CHTMLSpanElement>(\".counter\");\n    const resultElement = document.querySelector\u003CHTMLSpanElement>(\".result\");\n\n    if (!counterElements?.length || !resultElement) {\n        return;\n    }\n\n    counterElements.forEach((element) => {\n        element.innerText = counter.value.toString();\n    });\n\n    resultElement.innerText = result.value.toString();\n}\n\nconst button = document.querySelector\u003CHTMLButtonElement>(\"button\");\n\nbutton?.addEventListener(\"click\", () => {\n    counter.value++;\n});\n\nwatch(counter, () => {\n    setCounter();\n});\n",[22,27443,27444,27452,27465,27481,27503,27507,27531,27535,27539,27543,27555,27559,27571,27575,27581,27585,27593,27615,27637,27641,27659,27665,27669,27673,27689,27701,27705,27709,27721,27725,27729,27751,27755,27771,27779,27783,27787,27799,27806],{"__ignoreMap":133},[137,27445,27446,27448,27450],{"class":139,"line":140},[137,27447,10287],{"class":143},[137,27449,26898],{"class":284},[137,27451,3276],{"class":157},[137,27453,27454,27456,27459,27461,27463],{"class":139,"line":173},[137,27455,10287],{"class":143},[137,27457,27458],{"class":157}," { ref, computed, watch } ",[137,27460,10954],{"class":143},[137,27462,11091],{"class":284},[137,27464,3276],{"class":157},[137,27466,27467,27469,27471,27473,27475,27477,27479],{"class":139,"line":188},[137,27468,3077],{"class":143},[137,27470,26921],{"class":364},[137,27472,151],{"class":143},[137,27474,10468],{"class":147},[137,27476,356],{"class":157},[137,27478,6065],{"class":364},[137,27480,1502],{"class":157},[137,27482,27483,27485,27487,27489,27491,27493,27495,27497,27499,27501],{"class":139,"line":269},[137,27484,3077],{"class":143},[137,27486,26939],{"class":364},[137,27488,151],{"class":143},[137,27490,26944],{"class":147},[137,27492,3193],{"class":157},[137,27494,222],{"class":143},[137,27496,26951],{"class":157},[137,27498,7672],{"class":143},[137,27500,8023],{"class":364},[137,27502,1502],{"class":157},[137,27504,27505],{"class":139,"line":278},[137,27506,516],{"emptyLinePlaceholder":515},[137,27508,27509,27511,27513,27515,27517,27519,27521,27523,27525,27527,27529],{"class":139,"line":291},[137,27510,26966],{"class":157},[137,27512,3873],{"class":147},[137,27514,4033],{"class":157},[137,27516,26973],{"class":147},[137,27518,26976],{"class":157},[137,27520,4514],{"class":284},[137,27522,14105],{"class":157},[137,27524,17393],{"class":143},[137,27526,26985],{"class":157},[137,27528,253],{"class":143},[137,27530,3778],{"class":284},[137,27532,27533],{"class":139,"line":297},[137,27534,26994],{"class":284},[137,27536,27537],{"class":139,"line":302},[137,27538,26999],{"class":284},[137,27540,27541],{"class":139,"line":662},[137,27542,27004],{"class":284},[137,27544,27545,27547,27549,27551,27553],{"class":139,"line":667},[137,27546,27009],{"class":284},[137,27548,27012],{"class":157},[137,27550,1017],{"class":284},[137,27552,5414],{"class":157},[137,27554,27019],{"class":284},[137,27556,27557],{"class":139,"line":786},[137,27558,21366],{"class":284},[137,27560,27561,27563,27565,27567,27569],{"class":139,"line":798},[137,27562,27028],{"class":284},[137,27564,27012],{"class":157},[137,27566,1017],{"class":284},[137,27568,5414],{"class":157},[137,27570,27037],{"class":284},[137,27572,27573],{"class":139,"line":803},[137,27574,27042],{"class":284},[137,27576,27577,27579],{"class":139,"line":931},[137,27578,22056],{"class":284},[137,27580,3276],{"class":157},[137,27582,27583],{"class":139,"line":1568},[137,27584,516],{"emptyLinePlaceholder":515},[137,27586,27587,27589,27591],{"class":139,"line":1573},[137,27588,483],{"class":143},[137,27590,27059],{"class":147},[137,27592,275],{"class":157},[137,27594,27595,27597,27599,27601,27603,27605,27607,27609,27611,27613],{"class":139,"line":1578},[137,27596,4177],{"class":143},[137,27598,27068],{"class":364},[137,27600,151],{"class":143},[137,27602,3717],{"class":157},[137,27604,27075],{"class":147},[137,27606,4033],{"class":157},[137,27608,27080],{"class":147},[137,27610,26976],{"class":157},[137,27612,27085],{"class":284},[137,27614,1502],{"class":157},[137,27616,27617,27619,27621,27623,27625,27627,27629,27631,27633,27635],{"class":139,"line":1588},[137,27618,4177],{"class":143},[137,27620,27094],{"class":364},[137,27622,151],{"class":143},[137,27624,3717],{"class":157},[137,27626,3873],{"class":147},[137,27628,4033],{"class":157},[137,27630,27080],{"class":147},[137,27632,26976],{"class":157},[137,27634,27109],{"class":284},[137,27636,1502],{"class":157},[137,27638,27639],{"class":139,"line":1601},[137,27640,516],{"emptyLinePlaceholder":515},[137,27642,27643,27645,27647,27649,27651,27653,27655,27657],{"class":139,"line":3802},[137,27644,24696],{"class":143},[137,27646,158],{"class":157},[137,27648,17393],{"class":143},[137,27650,27126],{"class":157},[137,27652,8611],{"class":364},[137,27654,24707],{"class":143},[137,27656,27133],{"class":143},[137,27658,27136],{"class":157},[137,27660,27661,27663],{"class":139,"line":3808},[137,27662,5472],{"class":143},[137,27664,3276],{"class":157},[137,27666,27667],{"class":139,"line":3822},[137,27668,294],{"class":157},[137,27670,27671],{"class":139,"line":3827},[137,27672,516],{"emptyLinePlaceholder":515},[137,27674,27675,27677,27679,27681,27683,27685,27687],{"class":139,"line":3832},[137,27676,27155],{"class":157},[137,27678,8564],{"class":147},[137,27680,2774],{"class":157},[137,27682,27162],{"class":161},[137,27684,219],{"class":157},[137,27686,222],{"class":143},[137,27688,256],{"class":157},[137,27690,27691,27693,27695,27697,27699],{"class":139,"line":3840},[137,27692,27173],{"class":157},[137,27694,253],{"class":143},[137,27696,27178],{"class":157},[137,27698,7692],{"class":147},[137,27700,924],{"class":157},[137,27702,27703],{"class":139,"line":3846},[137,27704,2832],{"class":157},[137,27706,27707],{"class":139,"line":3861},[137,27708,516],{"emptyLinePlaceholder":515},[137,27710,27711,27713,27715,27717,27719],{"class":139,"line":3883},[137,27712,27195],{"class":157},[137,27714,253],{"class":143},[137,27716,27200],{"class":157},[137,27718,7692],{"class":147},[137,27720,924],{"class":157},[137,27722,27723],{"class":139,"line":3896},[137,27724,510],{"class":157},[137,27726,27727],{"class":139,"line":3901},[137,27728,516],{"emptyLinePlaceholder":515},[137,27730,27731,27733,27735,27737,27739,27741,27743,27745,27747,27749],{"class":139,"line":3906},[137,27732,3077],{"class":143},[137,27734,27219],{"class":364},[137,27736,151],{"class":143},[137,27738,3717],{"class":157},[137,27740,3873],{"class":147},[137,27742,4033],{"class":157},[137,27744,27230],{"class":147},[137,27746,26976],{"class":157},[137,27748,27235],{"class":284},[137,27750,1502],{"class":157},[137,27752,27753],{"class":139,"line":3911},[137,27754,516],{"emptyLinePlaceholder":515},[137,27756,27757,27759,27761,27763,27765,27767,27769],{"class":139,"line":4666},[137,27758,27246],{"class":157},[137,27760,4412],{"class":147},[137,27762,356],{"class":157},[137,27764,26258],{"class":284},[137,27766,4420],{"class":157},[137,27768,222],{"class":143},[137,27770,256],{"class":157},[137,27772,27773,27775,27777],{"class":139,"line":4672},[137,27774,27263],{"class":157},[137,27776,12683],{"class":143},[137,27778,3276],{"class":157},[137,27780,27781],{"class":139,"line":4680},[137,27782,5422],{"class":157},[137,27784,27785],{"class":139,"line":4711},[137,27786,516],{"emptyLinePlaceholder":515},[137,27788,27789,27792,27795,27797],{"class":139,"line":4716},[137,27790,27791],{"class":147},"watch",[137,27793,27794],{"class":157},"(counter, () ",[137,27796,222],{"class":143},[137,27798,256],{"class":157},[137,27800,27801,27804],{"class":139,"line":4721},[137,27802,27803],{"class":147},"    setCounter",[137,27805,924],{"class":157},[137,27807,27808],{"class":139,"line":4727},[137,27809,5422],{"class":157},[27,27811,27812,27813,164,27816,14528,27818,20514,27820,1017],{},"As we can see from the code above, this code is very similar to the code we implemented with Preact signals. The only difference here is that we import ",[22,27814,27815],{},"ref",[22,27817,27292],{},[22,27819,27791],{},[22,27821,27822],{},"vue",[27,27824,27825,27826,9060,27828,27830,27831,27833,27834,27836,27837,27839,27840,27842,27843,27845],{},"Here are the differences: in Vue, we use ",[22,27827,27815],{},[22,27829,27289],{}," to define a reactive variable. To update a ",[22,27832,27815],{}," variable, simply access its ",[22,27835,27330],{}," property, just as we did in Preact. The ",[22,27838,27292],{}," function behaves the same way, and the ",[22,27841,27791],{}," helps us watch a ",[22,27844,27815],{}," variable change simply by passing the variable as a parameter. In the callback function, we add logic that will be executed once the variable has changed.",[27,27847,27848],{},[63,27849],{"alt":27850,"src":27851},"demo in animated video gif","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fw_750\u002Fv1682415127\u002Fblog\u002Fsignals-in-vanilla-js\u002Fsignals-in-vanilla-js_uecikt",[27,27853,27854,27855,1017],{},"I hope you find this article helpful. You can find code examples in the following GitHub repository ",[45,27856,10647],{"href":27857,"target":2716,"rel":27858},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fsignals-in-vanilla-js",[2718,2719],[2617,27860,27861],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":133,"searchDepth":173,"depth":173,"links":27863},[27864,27865,27866],{"id":26615,"depth":173,"text":26616},{"id":26863,"depth":173,"text":26864},{"id":27415,"depth":173,"text":27416},"If you're a JavaScript developer, you may have heard about the buzz around signals lately. Several popular JavaScript frameworks, such as Vue, Preact, Solid, Angular, and Quick, have recently implemented and supported the use of signals. But what exactly are signals? As web applications become more complex, managing the state of JavaScript applications becomes a challenge. This is where signals can be a valuable tool, allowing the application to respond to changes in the state, such as a variable change. In this article, we will explore the concept of signals and how they can be used in Vanilla JavaScript applications. We will use the Vue and Preact reactivity libraries and see how easy and similar these two approaches are to implement. For the demo project, we will use Vite. So let's get started by creating a Vite app.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1682415127\u002Fblog\u002Fsignals-in-vanilla-js\u002Fsignals-in-vanilla-js-hero_lsdah8",[26546,27870,27871,27872,27873,27874,27815,27822,27875],"signals","javascript","vite","typescript","reactivity","preact",{},"\u002F2023\u002F04\u002F25\u002Fsignals-in-vanilla-js","25th April 2023",{"title":26546,"description":27867},"2023\u002F04\u002F25\u002Fsignals-in-vanilla-js","1CcX9qfc6zMNWIg8OVYjnGVXVlVHUw_WUe_MqO2XyS8",{"id":27883,"title":27884,"articleTags":27885,"author":11,"blog":12,"body":27888,"description":28592,"extension":2649,"image":28593,"keywords":28594,"meta":28604,"navigation":515,"path":28605,"published":28606,"readTime":1568,"seo":28607,"stem":28608,"type":2662,"__hash__":28609},"content\u002F2023\u002F10\u002F18\u002Ffrom-pixels-to-words-exploring-the-capabilities-of-image-to-text-ai-tools.md","From Pixels to Words - Exploring the Capabilities of Image-to-Text AI Tools",[27886,27887,9],"AI","LangChain",{"type":14,"value":27889,"toc":28579},[27890,27894,27908,27910,27914,27919,27922,27925,27931,27948,27951,27954,27960,27968,27971,27975,27993,27996,27999,28002,28005,28008,28011,28015,28034,28037,28132,28135,28138,28145,28153,28229,28232,28235,28239,28248,28279,28282,28285,28309,28312,28315,28319,28322,28325,28334,28337,28528,28531,28534,28537,28546,28549,28552,28555,28557,28560,28568,28576],[17,27891,27893],{"id":27892},"from-pixels-to-words-exploring-the-capabilities-of-image-to-text-ai-tools","From Pixels to Words: Exploring the Capabilities of Image-to-Text AI Tools",[27,27895,27896],{},[30,27897,27898,36,27900,40,27902],{},[33,27899],{"value":35},[33,27901],{"value":39},[42,27903,27904],{},[45,27905,27906],{"href":47},[33,27907],{"value":50},[52,27909],{":tags":54},[56,27911],{":audio-src":27912,":transcript-src":27913},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2023\u002F10\u002F18\u002Ffrom-pixels-to-words-exploring-the-capabilities-of-image-to-text-ai-tools\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2023\u002F10\u002F18\u002Ffrom-pixels-to-words-exploring-the-capabilities-of-image-to-text-ai-tools\u002Fsummary.json",[27,27915,27916],{},[63,27917],{"alt":12847,"src":27918},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1697369313\u002Fblog\u002Ffrom-pixels-to-words\u002Fhero",[27,27920,27921],{},"I had an idea to create an image-to-text automation tool for writing descriptions for my images. As a hobby landscape photographer, I have a lot of images, and this tool would speed up my workflow when posting on Instagram. I thought, \"Well, I am a software developer. I can develop something like that very quickly, right?\" I only need to add my credit card details to OpenAI, and then I can start using their API. This sounds simple enough. Well, let's discuss how it all went.",[27,27923,27924],{},"Imagine attaching an image and then starting a conversation with an AI assistant about the description you need help writing for that image. It would look something like this.",[27,27926,27927],{},[63,27928],{"alt":27929,"src":27930},"Entire conversation","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1697368221\u002Fblog\u002Ffrom-pixels-to-words\u002Ffrom-pixels-to-words-conversation",[2569,27932,27933,27936,27939,27942,27945],{},[1006,27934,27935],{},"First, we attach an image and ask the AI assistant for help.",[1006,27937,27938],{},"We request assistance in writing a description for the image, and the assistant provides one.",[1006,27940,27941],{},"Next, we ask the assistant to convert the description into an Instagram caption. We can see it adds emojis and hashtags as well.",[1006,27943,27944],{},"Then we also request an alternative text for the image to use on our website.",[1006,27946,27947],{},"Finally, we thank the assistant for their help.",[27,27949,27950],{},"What I just showed you is a screenshot of the app I developed, which includes a conversation with one of my images attached. The image was taken in 2016, and it shows the city of Melbourne. At the end of this document, I will provide you with the GitHub repo and a link to play with the app. But first, let's walk through the steps on how I achieved all of this.",[27,27952,27953],{},"Here is a diagram that visually represents the web app described above.",[27,27955,27956],{},[63,27957],{"alt":27958,"src":27959},"Diagram Chart","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1697368216\u002Fblog\u002Ffrom-pixels-to-words\u002Fdiagram-chart",[27,27961,27962,27963,27967],{},"The diagram above provides a high-level overview of the app's functionality. The user submits a question about an attached image, and the web application responds with an answer. To achieve this, our app utilises ",[45,27964,27887],{"href":27965,"target":2716,"rel":27966},"https:\u002F\u002Fjs.langchain.com\u002Fdocs\u002F",[2718,2719],", a powerful framework for working with Large Language Models (LLMs). LangChain supports both TypeScript and Python programming languages, and in our case, we are using TypeScript. We will become more familiar with LangChain and why we have chosen to work with it in the upcoming sections.",[27,27969,27970],{},"In addition, our app also uses additional tools to help us analyse the content of the image, giving us possible labels of classes or objects that might be included in the image.",[104,27972,27974],{"id":27973},"image-recognition-models-for-text-generation-and-object-detection","Image Recognition Models for Text Generation and Object Detection",[27,27976,27977,27978,114,27983,20514,27988,1017],{},"First, we will discuss the additional tools used to help us identify the content of the image. The LLM does not have any knowledge about the content of the image, so to write an effective description, we need to provide relevant information. We used two libraries to accomplish that: ",[45,27979,27982],{"href":27980,"target":2716,"rel":27981},"https:\u002F\u002Fwww.tensorflow.org\u002F",[2718,2719],"TensorFlow",[45,27984,27987],{"href":27985,"target":2716,"rel":27986},"https:\u002F\u002Fhuggingface.co\u002Fdocs\u002Ftransformers.js\u002Findex",[2718,2719],"Transformers.js",[45,27989,27992],{"href":27990,"target":2716,"rel":27991},"https:\u002F\u002Fhuggingface.co\u002F",[2718,2719],"Hugging Face",[123,27994,27982],{"id":27995},"tensorflow",[27,27997,27998],{},"Our app utilises two pre-trained models from TensorFlow. These models help to classify the image into meaningful labels with probability percentage and assist in detecting objects in photos.",[123,28000,27987],{"id":28001},"transformersjs",[27,28003,28004],{},"We also use two more models from Transformers.js, one to help us convert an image into meaningful words, and another for zero-shot image classification. We will explain what zero-shot image classification means later in this section.",[27,28006,28007],{},"We decided to use multiple models because each model predicts different parts of the image differently. Then all the labels will be passed through the zero-shot image classification model that will help us determine which labels are highly likely to be included in the image.",[27,28009,28010],{},"To demonstrate the models, we will use the same image as in our first chat, a photograph of the city of Melbourne that I took in 2016.",[123,28012,28014],{"id":28013},"image-classification-with-tensorflow","Image Classification with TensorFlow",[27,28016,28017,28018,28023,28024,164,28027,164,28030,28033],{},"Once the image has been uploaded the first model we are passing through is the ",[45,28019,28022],{"href":28020,"target":2716,"rel":28021},"https:\u002F\u002Fgithub.com\u002Ftensorflow\u002Ftfjs-models\u002Ftree\u002Fmaster\u002Fmobilenet",[2718,2719],"Image Classification"," model from TensorFlow. This TensorFlow.js model does not require knowledge of machine learning. It can take any browser-based image elements (",[22,28025,28026],{},"\u003Cimg>",[22,28028,28029],{},"\u003Cvideo>",[22,28031,28032],{},"\u003Ccanvas>",", for example) as input and return an array of the most likely predictions and their confidences.",[27,28035,28036],{},"The array of classes and probabilities for our image can be seen below:",[128,28038,28040],{"className":130,"code":28039,"language":132,"meta":133,"style":133},"[\n    {\n        className: \"castle\",\n        probability: 0.21438449621200562,\n    },\n    {\n        className: \"suspension bridge\",\n        probability: 0.1847757250070572,\n    },\n    {\n        className: \"lakeside, lakeshore\",\n        probability: 0.18075722455978394,\n    },\n];\n",[22,28041,28042,28047,28052,28062,28072,28076,28080,28089,28098,28102,28106,28115,28124,28128],{"__ignoreMap":133},[137,28043,28044],{"class":139,"line":140},[137,28045,28046],{"class":157},"[\n",[137,28048,28049],{"class":139,"line":173},[137,28050,28051],{"class":157},"    {\n",[137,28053,28054,28057,28060],{"class":139,"line":188},[137,28055,28056],{"class":157},"        className: ",[137,28058,28059],{"class":284},"\"castle\"",[137,28061,1961],{"class":157},[137,28063,28064,28067,28070],{"class":139,"line":269},[137,28065,28066],{"class":157},"        probability: ",[137,28068,28069],{"class":364},"0.21438449621200562",[137,28071,1961],{"class":157},[137,28073,28074],{"class":139,"line":278},[137,28075,775],{"class":157},[137,28077,28078],{"class":139,"line":291},[137,28079,28051],{"class":157},[137,28081,28082,28084,28087],{"class":139,"line":297},[137,28083,28056],{"class":157},[137,28085,28086],{"class":284},"\"suspension bridge\"",[137,28088,1961],{"class":157},[137,28090,28091,28093,28096],{"class":139,"line":302},[137,28092,28066],{"class":157},[137,28094,28095],{"class":364},"0.1847757250070572",[137,28097,1961],{"class":157},[137,28099,28100],{"class":139,"line":662},[137,28101,775],{"class":157},[137,28103,28104],{"class":139,"line":667},[137,28105,28051],{"class":157},[137,28107,28108,28110,28113],{"class":139,"line":786},[137,28109,28056],{"class":157},[137,28111,28112],{"class":284},"\"lakeside, lakeshore\"",[137,28114,1961],{"class":157},[137,28116,28117,28119,28122],{"class":139,"line":798},[137,28118,28066],{"class":157},[137,28120,28121],{"class":364},"0.18075722455978394",[137,28123,1961],{"class":157},[137,28125,28126],{"class":139,"line":803},[137,28127,775],{"class":157},[137,28129,28130],{"class":139,"line":931},[137,28131,5727],{"class":157},[27,28133,28134],{},"Let's analyse the results above. The highest probability was for a castle, which is inaccurate since there is no castle in the image. The second highest probability was for a suspension bridge, which is correct since there is a bridge in the photo. The final prediction was for a lakeside or lake shore, which is also incorrect because there is a river in the image, not a lake.",[27,28136,28137],{},"Therefore, the only useful label from the predictions above is suspension bridge.",[123,28139,28141,28144],{"id":28140},"object-detection-with-tensorflow",[42,28142,28143],{},"Object Detection"," with TensorFlow",[27,28146,28147,28148,28152],{},"We pass the image through the ",[45,28149,28143],{"href":28150,"target":2716,"rel":28151},"https:\u002F\u002Fgithub.com\u002Ftensorflow\u002Ftfjs-models\u002Ftree\u002Fmaster\u002Fcoco-ssd",[2718,2719]," model from TensorFlow as a second step. This model aims to identify multiple objects in a single image and is capable of detecting 80 different classes of objects. Unfortunately, in the case of the image of the city of Melbourne, the model was not able to detect any objects, resulting in an empty array. For demonstration purposes, the following example shows how the output would appear if we used an image in which objects could be detected:",[128,28154,28156],{"className":130,"code":28155,"language":132,"meta":133,"style":133},"[\n    {\n        bbox: [x, y, width, height],\n        class: \"person\",\n        score: 0.8380282521247864,\n    },\n    {\n        bbox: [x, y, width, height],\n        class: \"kite\",\n        score: 0.74644153267145157,\n    },\n];\n",[22,28157,28158,28162,28166,28171,28181,28191,28195,28199,28203,28212,28221,28225],{"__ignoreMap":133},[137,28159,28160],{"class":139,"line":140},[137,28161,28046],{"class":157},[137,28163,28164],{"class":139,"line":173},[137,28165,28051],{"class":157},[137,28167,28168],{"class":139,"line":188},[137,28169,28170],{"class":157},"        bbox: [x, y, width, height],\n",[137,28172,28173,28176,28179],{"class":139,"line":269},[137,28174,28175],{"class":157},"        class: ",[137,28177,28178],{"class":284},"\"person\"",[137,28180,1961],{"class":157},[137,28182,28183,28186,28189],{"class":139,"line":278},[137,28184,28185],{"class":157},"        score: ",[137,28187,28188],{"class":364},"0.8380282521247864",[137,28190,1961],{"class":157},[137,28192,28193],{"class":139,"line":291},[137,28194,775],{"class":157},[137,28196,28197],{"class":139,"line":297},[137,28198,28051],{"class":157},[137,28200,28201],{"class":139,"line":302},[137,28202,28170],{"class":157},[137,28204,28205,28207,28210],{"class":139,"line":662},[137,28206,28175],{"class":157},[137,28208,28209],{"class":284},"\"kite\"",[137,28211,1961],{"class":157},[137,28213,28214,28216,28219],{"class":139,"line":667},[137,28215,28185],{"class":157},[137,28217,28218],{"class":364},"0.74644153267145157",[137,28220,1961],{"class":157},[137,28222,28223],{"class":139,"line":786},[137,28224,775],{"class":157},[137,28226,28227],{"class":139,"line":798},[137,28228,5727],{"class":157},[27,28230,28231],{},"As we can see from the example above, this mode returns an array of bounding boxes with class names and confidence levels. Unfortunately, in our case, we received an empty array, so we did not get any predictions for our particular image.",[27,28233,28234],{},"Let's move on to the next models from Transformers.js.",[123,28236,28238],{"id":28237},"image-captioning-with-transformersjs","Image captioning with Transformers.js",[27,28240,28241,28242,28247],{},"Image captioning is the process of generating a description or caption from an input image. This requires both natural language processing and computer vision to generate the caption. To accomplish this, we use a model called ",[45,28243,28246],{"href":28244,"target":2716,"rel":28245},"https:\u002F\u002Fhuggingface.co\u002Fnlpconnect\u002Fvit-gpt2-image-captioning",[2718,2719],"vit-gpt2-image-captioning"," from Transformers.js. Using this model, giving us the following output for the image above:",[128,28249,28251],{"className":130,"code":28250,"language":132,"meta":133,"style":133},"[\n    {\n        generated_text: \"a bridge over a river with a city\",\n    },\n];\n",[22,28252,28253,28257,28261,28271,28275],{"__ignoreMap":133},[137,28254,28255],{"class":139,"line":140},[137,28256,28046],{"class":157},[137,28258,28259],{"class":139,"line":173},[137,28260,28051],{"class":157},[137,28262,28263,28266,28269],{"class":139,"line":188},[137,28264,28265],{"class":157},"        generated_text: ",[137,28267,28268],{"class":284},"\"a bridge over a river with a city\"",[137,28270,1961],{"class":157},[137,28272,28273],{"class":139,"line":269},[137,28274,775],{"class":157},[137,28276,28277],{"class":139,"line":278},[137,28278,5727],{"class":157},[27,28280,28281],{},"As we can see from the text above, the capture is pretty accurate. We got fairly accurate results for this particular image, but for every image, the results are different. When experimenting with different images, I sometimes get results that are not as accurate as this one. However, the next step will be to extract the nouns and verbs from the sentence above. How can we do that?",[27,28283,28284],{},"I won't go into the details of this step, but we will be using Chat GPT to ask it to return an array of all the nouns and verbs from the generated text above. It works quite well. Here are the results returned by Chat GPT:",[128,28286,28288],{"className":130,"code":28287,"language":132,"meta":133,"style":133},"[\"bridge\", \"river\", \"city\"];\n",[22,28289,28290],{"__ignoreMap":133},[137,28291,28292,28294,28297,28299,28302,28304,28307],{"class":139,"line":140},[137,28293,5717],{"class":157},[137,28295,28296],{"class":284},"\"bridge\"",[137,28298,164],{"class":157},[137,28300,28301],{"class":284},"\"river\"",[137,28303,164],{"class":157},[137,28305,28306],{"class":284},"\"city\"",[137,28308,5727],{"class":157},[27,28310,28311],{},"Basically, we can use only these three labels from the last model prediction and skip all the TensorFlow model usage. However, as I mentioned earlier, this is not the case for every image. In some cases, the Image Classification mode from TensorFlow can give us more accurate results. Therefore, we will keep both models for better predictions.",[27,28313,28314],{},"Now that we have all the predictions in place, the next step is to figure out which predicted labels are really included in the image. As human beings, we can easily determine that, but in the case of automation, we need to find a way to handle this automatically without human input. This is where the zero-shot image classification model that I mentioned earlier comes into play.",[123,28316,28318],{"id":28317},"zero-shot-image-classification-with-transformersjs","Zero-shot image classification with Transformers.js",[27,28320,28321],{},"Zero Shot Classification is the task of predicting a class that wasn't seen by the model during training. In other words, a zero-shot model allows us to classify data that wasn't used to build the model. For example, imagine a child is asked to recognise a zebra in a zoo. The child has never seen a zebra before, but they have seen a horse. By telling the child that a zebra is very similar to a horse but with black and white stripes, the child can recognise a zebra easily. This is essentially how the model works.",[27,28323,28324],{},"This is the opposite of what we've done before. In previous models, we give them an image, and they give us predictions on what can be included in the image. In zero-shot image classification, we provide both text predictions and an image, and the model can tell us which texts are highly likely to be included in the image.",[27,28326,28327,28328,28333],{},"In our case, we will be using ",[45,28329,28332],{"href":28330,"target":2716,"rel":28331},"https:\u002F\u002Fhuggingface.co\u002Fopenai\u002Fclip-vit-base-patch32",[2718,2719],"clip-vit-base-patch32"," from Transformers.js, which utilises the CLIP model developed by researchers at OpenAI.",[27,28335,28336],{},"All predicted labels previously assigned to the image are passed to this model. As a result, we receive the following output:",[128,28338,28340],{"className":130,"code":28339,"language":132,"meta":133,"style":133},"[\n    {\n        score: 0.36474844813346863,\n        label: \"bridge\",\n    },\n    {\n        score: 0.22917157411575317,\n        label: \"suspension bridge\",\n    },\n    {\n        score: 0.223050057888031,\n        label: \"city\",\n    },\n    {\n        score: 0.1010189950466156,\n        label: \"river\",\n    },\n    {\n        score: 0.0377478264272213,\n        label: \" lakeshore\",\n    },\n    {\n        score: 0.02330002561211586,\n        label: \"lakeside\",\n    },\n    {\n        score: 0.020963076502084732,\n        label: \"castle\",\n    },\n];\n",[22,28341,28342,28346,28350,28359,28368,28372,28376,28385,28393,28397,28401,28410,28418,28422,28426,28435,28443,28447,28451,28460,28469,28473,28477,28486,28495,28499,28503,28512,28520,28524],{"__ignoreMap":133},[137,28343,28344],{"class":139,"line":140},[137,28345,28046],{"class":157},[137,28347,28348],{"class":139,"line":173},[137,28349,28051],{"class":157},[137,28351,28352,28354,28357],{"class":139,"line":188},[137,28353,28185],{"class":157},[137,28355,28356],{"class":364},"0.36474844813346863",[137,28358,1961],{"class":157},[137,28360,28361,28364,28366],{"class":139,"line":269},[137,28362,28363],{"class":157},"        label: ",[137,28365,28296],{"class":284},[137,28367,1961],{"class":157},[137,28369,28370],{"class":139,"line":278},[137,28371,775],{"class":157},[137,28373,28374],{"class":139,"line":291},[137,28375,28051],{"class":157},[137,28377,28378,28380,28383],{"class":139,"line":297},[137,28379,28185],{"class":157},[137,28381,28382],{"class":364},"0.22917157411575317",[137,28384,1961],{"class":157},[137,28386,28387,28389,28391],{"class":139,"line":302},[137,28388,28363],{"class":157},[137,28390,28086],{"class":284},[137,28392,1961],{"class":157},[137,28394,28395],{"class":139,"line":662},[137,28396,775],{"class":157},[137,28398,28399],{"class":139,"line":667},[137,28400,28051],{"class":157},[137,28402,28403,28405,28408],{"class":139,"line":786},[137,28404,28185],{"class":157},[137,28406,28407],{"class":364},"0.223050057888031",[137,28409,1961],{"class":157},[137,28411,28412,28414,28416],{"class":139,"line":798},[137,28413,28363],{"class":157},[137,28415,28306],{"class":284},[137,28417,1961],{"class":157},[137,28419,28420],{"class":139,"line":803},[137,28421,775],{"class":157},[137,28423,28424],{"class":139,"line":931},[137,28425,28051],{"class":157},[137,28427,28428,28430,28433],{"class":139,"line":1568},[137,28429,28185],{"class":157},[137,28431,28432],{"class":364},"0.1010189950466156",[137,28434,1961],{"class":157},[137,28436,28437,28439,28441],{"class":139,"line":1573},[137,28438,28363],{"class":157},[137,28440,28301],{"class":284},[137,28442,1961],{"class":157},[137,28444,28445],{"class":139,"line":1578},[137,28446,775],{"class":157},[137,28448,28449],{"class":139,"line":1588},[137,28450,28051],{"class":157},[137,28452,28453,28455,28458],{"class":139,"line":1601},[137,28454,28185],{"class":157},[137,28456,28457],{"class":364},"0.0377478264272213",[137,28459,1961],{"class":157},[137,28461,28462,28464,28467],{"class":139,"line":3802},[137,28463,28363],{"class":157},[137,28465,28466],{"class":284},"\" lakeshore\"",[137,28468,1961],{"class":157},[137,28470,28471],{"class":139,"line":3808},[137,28472,775],{"class":157},[137,28474,28475],{"class":139,"line":3822},[137,28476,28051],{"class":157},[137,28478,28479,28481,28484],{"class":139,"line":3827},[137,28480,28185],{"class":157},[137,28482,28483],{"class":364},"0.02330002561211586",[137,28485,1961],{"class":157},[137,28487,28488,28490,28493],{"class":139,"line":3832},[137,28489,28363],{"class":157},[137,28491,28492],{"class":284},"\"lakeside\"",[137,28494,1961],{"class":157},[137,28496,28497],{"class":139,"line":3840},[137,28498,775],{"class":157},[137,28500,28501],{"class":139,"line":3846},[137,28502,28051],{"class":157},[137,28504,28505,28507,28510],{"class":139,"line":3861},[137,28506,28185],{"class":157},[137,28508,28509],{"class":364},"0.020963076502084732",[137,28511,1961],{"class":157},[137,28513,28514,28516,28518],{"class":139,"line":3883},[137,28515,28363],{"class":157},[137,28517,28059],{"class":284},[137,28519,1961],{"class":157},[137,28521,28522],{"class":139,"line":3896},[137,28523,775],{"class":157},[137,28525,28526],{"class":139,"line":3901},[137,28527,5727],{"class":157},[27,28529,28530],{},"As seen from the results above, this model is capable of giving pretty accurate scores for all the labels and determining which are most likely to be included in the image. Based on my previous experimentation while developing this application, I realised that the top three to five labels are fairly accurate. For our app, we will only focus on the top four labels. Next, we will explore the capabilities of LangChain and the specific feature we used from this framework to generate a meaningful description.",[104,28532,27887],{"id":28533},"langchain",[27,28535,28536],{},"Working with LLMs can be challenging, especially when working on a complex web application. I find managing memory and writing clear instructions (prompts) to be the two most challenging aspects. Fortunately, LangChain provides a solution to both of these issues and more.",[27,28538,28539,28540,28545],{},"LangChain is an open-source framework that supports both Python and TypeScript. It allows you to build applications that use LLMs. LangChain provides a simple interface that makes it easy to connect LLMs to our application. One of the key features that LangChain provides to help manage different prompts is chains. LangChain has different types of chains, and the one we use in our application is called the ",[45,28541,28544],{"href":28542,"target":2716,"rel":28543},"https:\u002F\u002Fjs.langchain.com\u002Fdocs\u002Fmodules\u002Fchains\u002Fother_chains\u002Fmulti_prompt_chain",[2718,2719],"Multi-Prompt Chain",". This allows us to use more than one prompt template and pick an appropriate prompt based on the user's question.",[27,28547,28548],{},"To make this clearer, let me give an example of how this applies to our application. We use different prompt templates in our application for different purposes. For example, we use one prompt to give instructions to the model on how to write a description, another prompt to handle Instagram descriptions, and a third prompt for writing an alt text. Each of these prompt templates has different instructions. For instance, when we want the model to assist the user in writing an Instagram caption, we provide instructions to add some emojis and hashtags. So, when someone asks for writing a description or an alt text, different instructions are given. We can fine-tune the output based on specific prompt templates for specific scenarios. In my experience, writing multiple short prompts instead of one large single prompt drastically improves the quality of the model's output.",[27,28550,28551],{},"Memory is another concept that LangChain solves out of the box. For applications like chatbots, it is essential that they can remember previous conversations. However, by default, LLMs do not have any long-term memory unless you input the chat history. LangChain solves this problem by providing several options for dealing with chat history: keeping all conversations, keeping only the latest number of conversations, or summarising the conversation. In our app, we use the option to keep the latest number conversations, so we remember the last ten conversations.",[27,28553,28554],{},"These are only a small number of the features that LangChain can offer. I highly recommend checking the LangChain documentation to learn more about the awesome features this tool provides.",[104,28556,2567],{"id":2566},[27,28558,28559],{},"I enjoyed developing this chatbot that helps humans write their descriptions base on an image. This is a beta version, and I have plans to improve it in the future, but it's a good starting point. It's still not perfect and can sometimes get confused, but as technology progresses and new models become available, I'm confident we'll be able to get much better results.",[27,28561,28562,28563,28567],{},"If you want to try it, follow this link: ",[45,28564,10647],{"href":28565,"target":2716,"rel":28566},"https:\u002F\u002Fimage-to-text-ai-chat.vercel.app\u002F",[2718,2719],". All you need to do is provide your OpenAI API key at the bottom of the page.",[27,28569,28570,28571,28575],{},"For those who want to see the source code, it's available on GitHub at this link: ",[45,28572,10647],{"href":28573,"target":2716,"rel":28574},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002FImage-to-Text-AI-Chat",[2718,2719],". Please keep in mind that this only works on Chrome and Mozilla for now. Safari is not currently supported. I hope you enjoy it!",[2617,28577,28578],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":133,"searchDepth":173,"depth":173,"links":28580},[28581,28590,28591],{"id":27973,"depth":173,"text":27974,"children":28582},[28583,28584,28585,28586,28588,28589],{"id":27995,"depth":188,"text":27982},{"id":28001,"depth":188,"text":27987},{"id":28013,"depth":188,"text":28014},{"id":28140,"depth":188,"text":28587},"Object Detection with TensorFlow",{"id":28237,"depth":188,"text":28238},{"id":28317,"depth":188,"text":28318},{"id":28533,"depth":173,"text":27887},{"id":2566,"depth":173,"text":2567},"I had an idea to create an image-to-text automation tool for writing descriptions for my images. As a hobby landscape photographer, I have a lot of images, and this tool would speed up my workflow when posting on Instagram. I thought, \"Well, I am a software developer. I can develop something like that very quickly, right?\" I only need to add my credit card details to OpenAI, and then I can start using their API. This sounds simple enough. Well, let's discuss how it all went. Imagine attaching an image and then starting a conversation with an AI assistant about the description you need help writing for that image. It would look something like this.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1697369313\u002Fblog\u002Ffrom-pixels-to-words\u002Fhero",[27887,27886,9,28595,28596,28597,28598,28022,28599,28600,28601,27982,27987,8,22707,12817,5300,5299,21096,28602,28603],"Artificial Intelligence","ML","Machine Learning","Chat GPT","Image Captioning","Image recognition","Zero-shot image classification","Chat","Chatbot",{},"\u002F2023\u002F10\u002F18\u002Ffrom-pixels-to-words-exploring-the-capabilities-of-image-to-text-ai-tools","18th October 2023",{"title":27884,"description":28592},"2023\u002F10\u002F18\u002Ffrom-pixels-to-words-exploring-the-capabilities-of-image-to-text-ai-tools","pHjS2GlCVpaE1KNRqWp5dYJeXQhDeuXwxTCVSFqSMNA",{"id":28611,"title":28612,"articleTags":28613,"author":11,"blog":12,"body":28615,"description":28997,"extension":2649,"image":28998,"keywords":28999,"meta":29000,"navigation":515,"path":29001,"published":29002,"readTime":302,"seo":29003,"stem":29004,"type":2662,"__hash__":29005},"content\u002F2023\u002F10\u002F23\u002Fgrouping-images-by-colours-exploring-colour-based-image-clustering.md","Grouping images by Colours - Exploring Colour-Based Image Clustering",[28614,27887,27886],"Python",{"type":14,"value":28616,"toc":28989},[28617,28620,28634,28636,28640,28645,28648,28651,28654,28657,28661,28664,28670,28673,28679,28682,28686,28689,28724,28727,28733,28737,28753,28756,28762,28780,28784,28787,28790,28798,28801,28807,28810,28813,28911,28914,28920,28924,28930,28933,28937,28940,28954,28957,28961,28986],[17,28618,28612],{"id":28619},"grouping-images-by-colours-exploring-colour-based-image-clustering",[27,28621,28622],{},[30,28623,28624,36,28626,40,28628],{},[33,28625],{"value":35},[33,28627],{"value":39},[42,28629,28630],{},[45,28631,28632],{"href":47},[33,28633],{"value":50},[52,28635],{":tags":54},[56,28637],{":audio-src":28638,":transcript-src":28639},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2023\u002F10\u002F23\u002Fgrouping-images-by-colours-exploring-colour-based-image-clustering\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2023\u002F10\u002F23\u002Fgrouping-images-by-colours-exploring-colour-based-image-clustering\u002Fsummary.json",[27,28641,28642],{},[63,28643],{"alt":12847,"src":28644},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1697956564\u002Fblog\u002Fgrouping-images-by-colours\u002Fot79c0slnfykfnvy5i9e.jpg",[27,28646,28647],{},"The idea for this experimental project, which I am going to demonstrate in this blog article, came to me from the Office Works website. Here's how it all happened.",[27,28649,28650],{},"As a hobbyist landscape photographer, I have accumulated hundreds of images over the years. Recently, I decided to print these photos in a 10x12\" photo book. I immediately checked the Office Works website to see if they offer such a service, and I was in luck. Office Works has an incredible platform that allows customers to easily create photo books using their online editor.",[27,28652,28653],{},"While I was able to accomplish what I wanted, there was one feature that I was missing but would be nice to have: the ability to sort all of the images based on their colours. In my case, I uploaded 160 images, and manually sorting all of them was a bit tedious.",[27,28655,28656],{},"As a software developer, I thought that this would be an interesting side project to see how far I could go in implementing this kind of sorting and grouping of images based on their colours hues. So here is how it all went.",[104,28658,28660],{"id":28659},"the-problem","The Problem",[27,28662,28663],{},"To demonstrate this example, I will use the 10 photos shown in the image below:",[27,28665,28666],{},[63,28667],{"alt":28668,"src":28669},"Original Images","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1697965697\u002Fblog\u002Fgrouping-images-by-colours\u002Fknfc7u1v025fbbuycq2m",[27,28671,28672],{},"The photos shown in the image are not sorted based on their hues, resulting in an unpleasing visual arrangement. It would be better if the photos were categorised into three groups based on their colour hues, as demonstrated in the subsequent image.",[27,28674,28675],{},[63,28676],{"alt":28677,"src":28678},"Groped Images","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1697965670\u002Fblog\u002Fgrouping-images-by-colours\u002Fhpepiyqamctzegdz8ttn",[27,28680,28681],{},"While it is not a big deal when we have just 10 images, as we can easily group them manually, it becomes a much bigger job when we have 160, as was the case for me. Some sort of automation would be very helpful in this situation.",[104,28683,28685],{"id":28684},"the-solution","The solution",[27,28687,28688],{},"For my solution to automatically sort images based on hues, I utilised several technologies. In the following sections, I will provide a detailed explanation of how each technology was implemented. Here is a brief overview of the technologies used:",[1003,28690,28691,28698,28705,28713],{},[1006,28692,28693,28697],{},[45,28694,28614],{"href":28695,"target":2716,"rel":28696},"https:\u002F\u002Fwww.python.org\u002F",[2718,2719]," (programming language)",[1006,28699,28700,28704],{},[45,28701,27887],{"href":28702,"target":2716,"rel":28703},"https:\u002F\u002Fwww.langchain.com\u002F",[2718,2719]," (framework for working with LLMs)",[1006,28706,28707,28712],{},[45,28708,28711],{"href":28709,"target":2716,"rel":28710},"https:\u002F\u002Fchat.openai.com\u002F",[2718,2719],"ChatGPT"," 3.5 turbo (LLM)",[1006,28714,28715,12753,28720,28723],{},[45,28716,28719],{"href":28717,"target":2716,"rel":28718},"https:\u002F\u002Fwww.sbert.net\u002F",[2718,2719],"SentenceTransformer",[22,28721,28722],{},"all-MiniLM-L6-v2"," open source model (used for generating embeddings to calculate similarities)",[27,28725,28726],{},"The diagram below provides an overview of the implementation. Lets analyse each step of the diagram in more detail below.",[27,28728,28729],{},[63,28730],{"alt":28731,"src":28732},"Diagram - Grouping images by Colours - Exploring Colour-Based Image Clustering","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1697966006\u002Fblog\u002Fgrouping-images-by-colours\u002Fn0d2cdowobpbp6tbk1xb",[123,28734,28736],{"id":28735},"extract-colours-from-a-photo-using-kmeans-clustering","Extract Colours from a photo using KMeans clustering",[27,28738,28739,28740,164,28743,164,28746,14528,28749,28752],{},"In the first step, each photo is loaded, and then each pixel is analysed using several Python libraries that help accomplish this, such a ",[22,28741,28742],{},"scikit-learn",[22,28744,28745],{},"opencv-python",[22,28747,28748],{},"numpy",[22,28750,28751],{},"pillow",". Then each pixel is assigned to one of the 5 clusters, represented by a hex value, using KMeans. Increasing the number of clusters results in more accurate classifications, but it also affects the processing speed, resulting in slower processing time. We stick with 5 clusters since they are more than enough for our purpose. I also tried classification with 10 clusters, but that resulted in doubling the processing time. I will share some time comparisons in the later section of this document.",[27,28754,28755],{},"The image below shows how the hues of a photo are grouped into 5 clusters with different hex colours values.",[27,28757,28758],{},[63,28759],{"alt":28760,"src":28761},"Clusters","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1697966134\u002Fblog\u002Fgrouping-images-by-colours\u002Fmkubxeofuui2juhl3k0y",[27,28763,28764,28765,28769,28770,28774,28775,1017],{},"This implementation that I used here is part of an open source project I found on Hugging Face ",[45,28766,10647],{"href":28767,"target":2716,"rel":28768},"https:\u002F\u002Fhuggingface.co\u002Fspaces\u002FShamima\u002Fextract-color-from-image",[2718,2719],". The author of the project, Shamima Hossain, has done an amazing job implementing this. She has also contributed to a lot of open source projects regarding AI and ML work. If you are interested, you can find her work on GitHub ",[45,28771,10647],{"href":28772,"target":2716,"rel":28773},"https:\u002F\u002Fgithub.com\u002Fsilvererudite",[2718,2719]," or follow her on ",[45,28776,28779],{"href":28777,"target":2716,"rel":28778},"https:\u002F\u002Ftwitter.com\u002FShamimaHossai13",[2718,2719],"Twitter",[104,28781,28783],{"id":28782},"colour-classification","Colour classification",[27,28785,28786],{},"To group the photos based on their most used hex colours, we need to consider that it is unlikely for two photos to have identical hex values. Therefore, we will categorise the photos into similar colour groups based on their hues.",[27,28788,28789],{},"To accomplish this task, we need a comprehensive list of colours that covers the entire spectrum. To obtain the list, I asked chat GPT to provide it for me. My exact question was as follows:",[27,28791,28792,28793],{},"\"",[42,28794,28795],{},[30,28796,28797],{},"Give me a list of colours different variations from all spectrum. I am going to use this colour labels to classify hex colours. Give me as many colour labels as possible. Including white and black variations.\"",[27,28799,28800],{},"The response was the following list of colours that are shown in the image below:",[27,28802,28803],{},[63,28804],{"alt":28805,"src":28806},"Entire Spectrum","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1697966229\u002Fblog\u002Fgrouping-images-by-colours\u002Fvtsor7rqjbxno6dux7gf",[27,28808,28809],{},"The next step is to categorise the hex values of each photo into one of the mentioned colours names.",[27,28811,28812],{},"To accomplish this task, I utilised the OpenAI function with the LangChain framework. The language model used was Chat GPT. The prompt and the structure of the JSON output were as follows:",[128,28814,28818],{"className":28815,"code":28816,"language":28817,"meta":133,"style":133},"language-python shiki shiki-themes github-light github-dark","[\n     (\n         \"system\",\n         \"You are a hex colour to colour name converter. You are given a hex colour and you must return the colour name. The hex colour must belong in one of the following descriptive colour labels: Red, Crimson, Scarlet, Vermilion, Maroon, Rose, Pink, Magenta, Fuchsia, Purple, Lavender, Indigo, Blue, Navy, Azure, Cyan, Teal, Turquoise, Green, Emerald, Lime, Chartreuse, Olive, Yellow, Gold, Amber, Orange, Peach, Apricot, Brown, Sienna, Chocolate, Tan, Beige, Khaki, Gray, Silver, Charcoal, White, Ivory, Cream, Pearl, Platinum, Jet Black, Onyx Black\",\n     ),\n     (\"human\", \"Use the given hex color to classify into a colour name: {input}\"),\n     (\"human\", \"Tip: Make sure to use the labels that were provided to classify the colour.\"),\n ]\n\njson_schema = {\n    \"title\": \"Colour\",\n    \"description\": \"Convert a hex colour to a colour name\",\n    \"type\": \"object\",\n    \"properties\": {\n        \"colour_name\": {\"type\": \"string\", \"description\": \"The colour name\"},\n        \"hex_colour\": {\"type\": \"string\", \"description\": \"The hex colour\"},\n    },\n    \"required\": [\"colour_name\"],\n}\n","python",[22,28819,28820,28824,28829,28834,28839,28844,28849,28854,28859,28863,28868,28873,28878,28883,28888,28893,28898,28902,28907],{"__ignoreMap":133},[137,28821,28822],{"class":139,"line":140},[137,28823,28046],{},[137,28825,28826],{"class":139,"line":173},[137,28827,28828],{},"     (\n",[137,28830,28831],{"class":139,"line":188},[137,28832,28833],{},"         \"system\",\n",[137,28835,28836],{"class":139,"line":269},[137,28837,28838],{},"         \"You are a hex colour to colour name converter. You are given a hex colour and you must return the colour name. The hex colour must belong in one of the following descriptive colour labels: Red, Crimson, Scarlet, Vermilion, Maroon, Rose, Pink, Magenta, Fuchsia, Purple, Lavender, Indigo, Blue, Navy, Azure, Cyan, Teal, Turquoise, Green, Emerald, Lime, Chartreuse, Olive, Yellow, Gold, Amber, Orange, Peach, Apricot, Brown, Sienna, Chocolate, Tan, Beige, Khaki, Gray, Silver, Charcoal, White, Ivory, Cream, Pearl, Platinum, Jet Black, Onyx Black\",\n",[137,28840,28841],{"class":139,"line":278},[137,28842,28843],{},"     ),\n",[137,28845,28846],{"class":139,"line":291},[137,28847,28848],{},"     (\"human\", \"Use the given hex color to classify into a colour name: {input}\"),\n",[137,28850,28851],{"class":139,"line":297},[137,28852,28853],{},"     (\"human\", \"Tip: Make sure to use the labels that were provided to classify the colour.\"),\n",[137,28855,28856],{"class":139,"line":302},[137,28857,28858],{}," ]\n",[137,28860,28861],{"class":139,"line":662},[137,28862,516],{"emptyLinePlaceholder":515},[137,28864,28865],{"class":139,"line":667},[137,28866,28867],{},"json_schema = {\n",[137,28869,28870],{"class":139,"line":786},[137,28871,28872],{},"    \"title\": \"Colour\",\n",[137,28874,28875],{"class":139,"line":798},[137,28876,28877],{},"    \"description\": \"Convert a hex colour to a colour name\",\n",[137,28879,28880],{"class":139,"line":803},[137,28881,28882],{},"    \"type\": \"object\",\n",[137,28884,28885],{"class":139,"line":931},[137,28886,28887],{},"    \"properties\": {\n",[137,28889,28890],{"class":139,"line":1568},[137,28891,28892],{},"        \"colour_name\": {\"type\": \"string\", \"description\": \"The colour name\"},\n",[137,28894,28895],{"class":139,"line":1573},[137,28896,28897],{},"        \"hex_colour\": {\"type\": \"string\", \"description\": \"The hex colour\"},\n",[137,28899,28900],{"class":139,"line":1578},[137,28901,775],{},[137,28903,28904],{"class":139,"line":1588},[137,28905,28906],{},"    \"required\": [\"colour_name\"],\n",[137,28908,28909],{"class":139,"line":1601},[137,28910,510],{},[27,28912,28913],{},"Based on the earlier shown image, we proceeded to group each hex colour using the Open AI function. The result was as follows:",[27,28915,28916],{},[63,28917],{"alt":28918,"src":28919},"Clusters-2","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1697966349\u002Fblog\u002Fgrouping-images-by-colours\u002Fgwpg8rwo4x9gutthnucr",[104,28921,28923],{"id":28922},"photo-group-classification-using-embeddings","Photo group classification using embeddings",[27,28925,28926,28927,28929],{},"The last step is to compare the similarities between each photo based on the colour grouping we did in the previous section. This can be easily accomplished by converting the colour names into embeddings and comparing them using a similarity score. To do this, we utilise a Python library called SentenceTransformer, along with an open-source model called ",[22,28928,28722],{},", which I have locally served.",[27,28931,28932],{},"The number of clusters was specified to be 3, since the number of photos in this example is relatively small (only 10 photos).",[17,28934,28936],{"id":28935},"performance","Performance",[27,28938,28939],{},"For this example, I used several different setups to measure the time it takes to run the photo grouping:",[1003,28941,28942,28945,28948,28951],{},[1006,28943,28944],{},"10 photos with a resolution of 1024px on the longer side and 5 colour clusters: 157.10 seconds",[1006,28946,28947],{},"10 photos with a resolution of 2048px on the longer side and 5 colour clusters: 210.68 seconds",[1006,28949,28950],{},"10 photos with a resolution of 2048px on the longer side and 10 colour clusters: 397.74 seconds",[1006,28952,28953],{},"14 photos with a resolution of 2048px on the longer side and 10 colour clusters: 1029.08 seconds",[27,28955,28956],{},"I used my personal machine, which has an Apple M1 Max with 64GB of memory. As expected, the performance was slower when using higher photo resolutions and more clusters.",[17,28958,28960],{"id":28959},"resources","Resources",[1003,28962,28963,28970,28976],{},[1006,28964,28965,28966],{},"Project GitHub repository: ",[45,28967,10647],{"href":28968,"target":2716,"rel":28969},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fimage_classification_based_on_colours",[2718,2719],[1006,28971,28972,28973],{},"Shamima Hossain - Extract colours from an image using KMeans clustering: ",[45,28974,10647],{"href":28767,"target":2716,"rel":28975},[2718,2719],[1006,28977,28978,28979,28981,28982],{},"Hugging Face - ",[22,28980,28722],{}," model: ",[45,28983,10647],{"href":28984,"target":2716,"rel":28985},"https:\u002F\u002Fhuggingface.co\u002Fsentence-transformers\u002Fall-MiniLM-L6-v2",[2718,2719],[2617,28987,28988],{},"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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":133,"searchDepth":173,"depth":173,"links":28990},[28991,28992,28995,28996],{"id":28659,"depth":173,"text":28660},{"id":28684,"depth":173,"text":28685,"children":28993},[28994],{"id":28735,"depth":188,"text":28736},{"id":28782,"depth":173,"text":28783},{"id":28922,"depth":173,"text":28923},"The idea for this experimental project, which I am going to demonstrate in this blog article, came to me from the Office Works website. Here's how it all happened. As a hobbyist landscape photographer, I have accumulated hundreds of images over the years. Recently, I decided to print these photos in a 10x12\" photo book. I immediately checked the Office Works website to see if they offer such a service, and I was in luck. Office Works has an incredible platform that allows customers to easily create photo books using their online editor. While I was able to accomplish what I wanted, there was one feature that I was missing but would be nice to have the ability to sort all of the images based on their colours. In my case, I uploaded 160 images, and manually sorting all of them was a bit tedious.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1697956564\u002Fblog\u002Fgrouping-images-by-colours\u002Fot79c0slnfykfnvy5i9e",[27887,27886,28614,28595,28596,28597,28598,28022,5300,5299],{},"\u002F2023\u002F10\u002F23\u002Fgrouping-images-by-colours-exploring-colour-based-image-clustering","23rd October 2023",{"title":28612,"description":28997},"2023\u002F10\u002F23\u002Fgrouping-images-by-colours-exploring-colour-based-image-clustering","0Knl67FfDv6HSH4iXY5MMBvJdXmmdsZwqhjrJdiWoh8",{"id":29007,"title":29008,"articleTags":29009,"author":11,"blog":12,"body":29010,"description":30729,"extension":2649,"image":30730,"keywords":30731,"meta":30735,"navigation":515,"path":30736,"published":30737,"readTime":667,"seo":30738,"stem":30739,"type":2662,"__hash__":30740},"content\u002F2024\u002F01\u002F23\u002Fhow-to-define-multiple-components-in-a-single-file-in-nuxt-using-jsx.md","How to Define Multiple Components in a Single File in Nuxt using JSX",[21096,8,12817],{"type":14,"value":29011,"toc":30710},[29012,29015,29029,29031,29035,29040,29046,29049,29053,29060,29077,29093,29096,29117,29123,29165,29172,29189,29213,29240,29247,29329,29337,29381,29387,29488,29532,29538,29605,29612,29615,29618,29621,29624,29653,29658,29713,29719,29736,29747,29769,29773,29780,29783,29811,29814,29821,29833,29939,29946,29951,29967,30071,30078,30083,30114,30165,30170,30318,30327,30340,30367,30373,30378,30386,30511,30532,30544,30550,30683,30692,30699,30701,30707],[17,29013,29008],{"id":29014},"how-to-define-multiple-components-in-a-single-file-in-nuxt-using-jsx",[27,29016,29017],{},[30,29018,29019,36,29021,40,29023],{},[33,29020],{"value":35},[33,29022],{"value":39},[42,29024,29025],{},[45,29026,29027],{"href":47},[33,29028],{"value":50},[52,29030],{":tags":54},[56,29032],{":audio-src":29033,":transcript-src":29034},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2024\u002F01\u002F23\u002Fhow-to-define-multiple-components-in-a-single-file-in-nuxt-using-jsx\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2024\u002F01\u002F23\u002Fhow-to-define-multiple-components-in-a-single-file-in-nuxt-using-jsx\u002Fsummary.json",[27,29036,29037],{},[63,29038],{"alt":12847,"src":29039},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1705811682\u002Fblog\u002Fhow-to-defining-multiple-components-in-a-single-file-in-nuxt\u002Fdx3lzu2av0hwbu2hkuob",[27,29041,29042,29043,29045],{},"I've been using Vue and Nuxt for quite a while now. One feature I've always been missing is the ability to declare more than one component in a single ",[22,29044,7575],{}," file. This has been possible in other frameworks like React for a long time. I know that most people will say that if you want more than one component, it's a good idea to separate those components into separate files. I agree with that statement and it makes sense. However, sometimes for very small components that won't be used in other places in the code, it also makes sense to declare them in the same file to make the code more readable.",[27,29047,29048],{},"I thought that it wasn't possible until I found out that we can actually do that. In this article, I'm going to demonstrate how we can declare more than one component in Nuxt 3 and provide a few examples. So keep reading.",[104,29050,29052],{"id":29051},"create-a-new-nuxt-project","Create a new Nuxt project.",[27,29054,29055,29056,29059],{},"First, we are going to create a brand new Nuxt 3 project. Execute the following command in your terminal and name your project ",[22,29057,29058],{},"nuxt-multiple-components",", or choose any other name that you prefer:",[128,29061,29063],{"className":8665,"code":29062,"language":8667,"meta":133,"style":133},"npx nuxi@latest init nuxt-multiple-components\n",[22,29064,29065],{"__ignoreMap":133},[137,29066,29067,29069,29072,29074],{"class":139,"line":140},[137,29068,21167],{"class":147},[137,29070,29071],{"class":284}," nuxi@latest",[137,29073,9539],{"class":284},[137,29075,29076],{"class":284}," nuxt-multiple-components\n",[27,29078,29079,29080,29082,29083,164,29085,29088,29089,29092],{},"I chose ",[22,29081,17263],{}," as my package manager, but you can choose between ",[22,29084,9536],{},[22,29086,29087],{},"pnpm",", or ",[22,29090,29091],{},"bun"," if you prefer those instead.",[27,29094,29095],{},"Once the project has been initialised, we need to install the dependencies and start the project using the following commands in the terminal:",[128,29097,29099],{"className":8665,"code":29098,"language":8667,"meta":133,"style":133},"cd nuxt-multiple-components\nyarn\nyarn dev\n\n",[22,29100,29101,29107,29111],{"__ignoreMap":133},[137,29102,29103,29105],{"class":139,"line":140},[137,29104,9558],{"class":364},[137,29106,29076],{"class":284},[137,29108,29109],{"class":139,"line":173},[137,29110,26731],{"class":147},[137,29112,29113,29115],{"class":139,"line":188},[137,29114,17263],{"class":147},[137,29116,9581],{"class":284},[27,29118,29119,29120,29122],{},"Great! Now our project is set up. Before diving into the code, let's do some clean-up. In the ",[22,29121,21801],{}," file, please delete the predefined component so that the code will look like the following:",[128,29124,29126],{"className":13299,"code":29125,"language":13301,"meta":133,"style":133},"\u003Cscript setup>\n\u003Cscript>\n\n\u003Ctemplate>\n\u003C\u002Ftemplate>\n\n",[22,29127,29128,29137,29145,29149,29157],{"__ignoreMap":133},[137,29129,29130,29132,29135],{"class":139,"line":140},[137,29131,4033],{"class":143},[137,29133,29134],{"class":157},"script setup",[137,29136,4053],{"class":143},[137,29138,29139,29141,29143],{"class":139,"line":173},[137,29140,4033],{"class":157},[137,29142,4037],{"class":147},[137,29144,4053],{"class":157},[137,29146,29147],{"class":139,"line":188},[137,29148,516],{"emptyLinePlaceholder":515},[137,29150,29151,29153,29155],{"class":139,"line":269},[137,29152,4033],{"class":157},[137,29154,7821],{"class":147},[137,29156,4053],{"class":157},[137,29158,29159,29161,29163],{"class":139,"line":278},[137,29160,4083],{"class":143},[137,29162,7821],{"class":157},[137,29164,4053],{"class":143},[104,29166,29168,29169,1017],{"id":29167},"define-our-first-component-in-jsx","Define our first component in ",[22,29170,29171],{},"JSX",[27,29173,29174,29175,29178,29179,114,29182,29185,29186,29188],{},"Some may wonder what JSX can do with Vue frameworks. Although JSX is a popular JavaScript syntax in ",[22,29176,29177],{},"React.js"," and frameworks that use React, such as ",[22,29180,29181],{},"Next",[22,29183,29184],{},"Remix",", we can technically write ",[22,29187,29171],{}," in Vue as well. This is what we will use to declare multiple components in a single file in Vue.",[27,29190,29191,29192,13477,29194,29197,29198,29201,29202,29204,29205,10928,29208,29210,29211,9772],{},"The very first thing we need to do is to define the language in the ",[22,29193,9591],{},[22,29195,29196],{},"jsx"," or, in our case here I prefer to use TypeScript and will be using ",[22,29199,29200],{},"tsx"," instead. So let's do that. In the ",[22,29203,21801],{}," file, set ",[22,29206,29207],{},"lang",[22,29209,29200],{}," in order to enable writing JSX code in our ",[22,29212,7575],{},[128,29214,29216],{"className":13299,"code":29215,"language":13301,"meta":133,"style":133},"\u003Cscript setup lang=\"tsx\">\n\u003Cscript>\n",[22,29217,29218,29232],{"__ignoreMap":133},[137,29219,29220,29222,29225,29227,29230],{"class":139,"line":140},[137,29221,4033],{"class":143},[137,29223,29224],{"class":157},"script setup lang",[137,29226,253],{"class":143},[137,29228,29229],{"class":284},"\"tsx\"",[137,29231,4053],{"class":143},[137,29233,29234,29236,29238],{"class":139,"line":173},[137,29235,4033],{"class":157},[137,29237,4037],{"class":147},[137,29239,4053],{"class":157},[27,29241,29242,29243,29246],{},"Then let's define our first ",[22,29244,29245],{},"\u003CCustomLink\u002F>"," JSX component:",[128,29248,29250],{"className":13299,"code":29249,"language":13301,"meta":133,"style":133},"\u003Cscript setup lang=\"tsx\">\n    const CustomLink = (props: { href: string; name: string }) => \u003Ca href=\"{props.href}>{props.name}\"\u003C\u002Fa>;\n\u003Cscript>\n",[22,29251,29252,29264,29321],{"__ignoreMap":133},[137,29253,29254,29256,29258,29260,29262],{"class":139,"line":140},[137,29255,4033],{"class":143},[137,29257,29224],{"class":157},[137,29259,253],{"class":143},[137,29261,29229],{"class":284},[137,29263,4053],{"class":143},[137,29265,29266,29268,29271,29273,29275,29278,29280,29282,29285,29287,29289,29291,29293,29295,29297,29300,29302,29305,29307,29309,29311,29314,29316,29318],{"class":139,"line":173},[137,29267,4177],{"class":143},[137,29269,29270],{"class":147}," CustomLink",[137,29272,151],{"class":143},[137,29274,158],{"class":157},[137,29276,29277],{"class":161},"props",[137,29279,894],{"class":143},[137,29281,8906],{"class":157},[137,29283,29284],{"class":161},"href",[137,29286,894],{"class":143},[137,29288,13630],{"class":364},[137,29290,2323],{"class":157},[137,29292,1387],{"class":161},[137,29294,894],{"class":143},[137,29296,13630],{"class":364},[137,29298,29299],{"class":157}," }) ",[137,29301,222],{"class":143},[137,29303,29304],{"class":157}," \u003C",[137,29306,45],{"class":147},[137,29308,23218],{"class":147},[137,29310,253],{"class":157},[137,29312,29313],{"class":284},"\"{props.href}>{props.name}\"",[137,29315,4083],{"class":157},[137,29317,45],{"class":147},[137,29319,29320],{"class":157},">;\n",[137,29322,29323,29325,29327],{"class":139,"line":188},[137,29324,4033],{"class":157},[137,29326,4037],{"class":147},[137,29328,4053],{"class":157},[27,29330,29331,29332,114,29334,29336],{},"We declare an anchor HTML element component that receives two props: ",[22,29333,29284],{},[22,29335,1387],{},". To use this component in our tablet, we follow the same approach as using other Vue components. Here is how we use it:",[128,29338,29340],{"className":4024,"code":29339,"language":4026,"meta":133,"style":133},"\u003Ctemplate>\n    \u003CCustomLink href=\"https:\u002F\u002Fvitejs.dev\" name=\"Vite\" \u002F>\n\u003C\u002Ftemplate>\n",[22,29341,29342,29350,29373],{"__ignoreMap":133},[137,29343,29344,29346,29348],{"class":139,"line":140},[137,29345,4033],{"class":157},[137,29347,7821],{"class":4036},[137,29349,4053],{"class":157},[137,29351,29352,29354,29357,29359,29361,29364,29366,29368,29371],{"class":139,"line":173},[137,29353,4072],{"class":157},[137,29355,29356],{"class":8180},"CustomLink",[137,29358,23218],{"class":147},[137,29360,253],{"class":157},[137,29362,29363],{"class":284},"\"https:\u002F\u002Fvitejs.dev\"",[137,29365,891],{"class":147},[137,29367,253],{"class":157},[137,29369,29370],{"class":284},"\"Vite\"",[137,29372,4078],{"class":157},[137,29374,29375,29377,29379],{"class":139,"line":188},[137,29376,4083],{"class":157},[137,29378,7821],{"class":4036},[137,29380,4053],{"class":157},[27,29382,29383,29384,29386],{},"We can even declare reactive ",[22,29385,27815],{}," variables as we usually do in the Vue composition API and pass those values as props without losing any reactivity:",[128,29388,29390],{"className":13299,"code":29389,"language":13301,"meta":133,"style":133},"\u003Cscript setup lang=\"tsx\">\n    const href = \"https:\u002F\u002Fvitejs.dev\";\n    const name = \"Vite\";\n    const CustomLink = (props: { href: string; name: string }) => \u003Ca href=\"{props.href}>{props.name}\"\u003C\u002Fa>;\n\u003Cscript>\n",[22,29391,29392,29404,29417,29430,29480],{"__ignoreMap":133},[137,29393,29394,29396,29398,29400,29402],{"class":139,"line":140},[137,29395,4033],{"class":143},[137,29397,29224],{"class":157},[137,29399,253],{"class":143},[137,29401,29229],{"class":284},[137,29403,4053],{"class":143},[137,29405,29406,29408,29410,29412,29415],{"class":139,"line":173},[137,29407,4177],{"class":143},[137,29409,23218],{"class":364},[137,29411,151],{"class":143},[137,29413,29414],{"class":284}," \"https:\u002F\u002Fvitejs.dev\"",[137,29416,3276],{"class":157},[137,29418,29419,29421,29423,29425,29428],{"class":139,"line":188},[137,29420,4177],{"class":143},[137,29422,891],{"class":364},[137,29424,151],{"class":143},[137,29426,29427],{"class":284}," \"Vite\"",[137,29429,3276],{"class":157},[137,29431,29432,29434,29436,29438,29440,29442,29444,29446,29448,29450,29452,29454,29456,29458,29460,29462,29464,29466,29468,29470,29472,29474,29476,29478],{"class":139,"line":269},[137,29433,4177],{"class":143},[137,29435,29270],{"class":147},[137,29437,151],{"class":143},[137,29439,158],{"class":157},[137,29441,29277],{"class":161},[137,29443,894],{"class":143},[137,29445,8906],{"class":157},[137,29447,29284],{"class":161},[137,29449,894],{"class":143},[137,29451,13630],{"class":364},[137,29453,2323],{"class":157},[137,29455,1387],{"class":161},[137,29457,894],{"class":143},[137,29459,13630],{"class":364},[137,29461,29299],{"class":157},[137,29463,222],{"class":143},[137,29465,29304],{"class":157},[137,29467,45],{"class":147},[137,29469,23218],{"class":147},[137,29471,253],{"class":157},[137,29473,29313],{"class":284},[137,29475,4083],{"class":157},[137,29477,45],{"class":147},[137,29479,29320],{"class":157},[137,29481,29482,29484,29486],{"class":139,"line":278},[137,29483,4033],{"class":157},[137,29485,4037],{"class":147},[137,29487,4053],{"class":157},[128,29489,29491],{"className":4024,"code":29490,"language":4026,"meta":133,"style":133},"\u003Ctemplate>\n    \u003CCustomLink :href=\"href\" :name=\"name\" \u002F>\n\u003C\u002Ftemplate>\n",[22,29492,29493,29501,29524],{"__ignoreMap":133},[137,29494,29495,29497,29499],{"class":139,"line":140},[137,29496,4033],{"class":157},[137,29498,7821],{"class":4036},[137,29500,4053],{"class":157},[137,29502,29503,29505,29507,29510,29512,29515,29518,29520,29522],{"class":139,"line":173},[137,29504,4072],{"class":157},[137,29506,29356],{"class":8180},[137,29508,29509],{"class":147}," :href",[137,29511,253],{"class":157},[137,29513,29514],{"class":284},"\"href\"",[137,29516,29517],{"class":147}," :name",[137,29519,253],{"class":157},[137,29521,5393],{"class":284},[137,29523,4078],{"class":157},[137,29525,29526,29528,29530],{"class":139,"line":188},[137,29527,4083],{"class":157},[137,29529,7821],{"class":4036},[137,29531,4053],{"class":157},[27,29533,29534,29535,29537],{},"The advantage of this is that we can combine regular Vue components, JSX components, and regular HTML all together in the same Vue template within a ",[22,29536,7575],{}," file. Therefore, the following example will still be valid syntax:",[128,29539,29541],{"className":4024,"code":29540,"language":4026,"meta":133,"style":133},"\u003Ctemplate>\n    \u003Cp>This is my first JSX component!\u003C\u002Fp>\n    \u003CCustomLink :href=\"href\" :name=\"name\" \u002F>\n    \u003Cp>Let me know what you think!\u003C\u002Fp>\n\u003C\u002Ftemplate>\n",[22,29542,29543,29551,29564,29584,29597],{"__ignoreMap":133},[137,29544,29545,29547,29549],{"class":139,"line":140},[137,29546,4033],{"class":157},[137,29548,7821],{"class":4036},[137,29550,4053],{"class":157},[137,29552,29553,29555,29557,29560,29562],{"class":139,"line":173},[137,29554,4072],{"class":157},[137,29556,27],{"class":4036},[137,29558,29559],{"class":157},">This is my first JSX component!\u003C\u002F",[137,29561,27],{"class":4036},[137,29563,4053],{"class":157},[137,29565,29566,29568,29570,29572,29574,29576,29578,29580,29582],{"class":139,"line":188},[137,29567,4072],{"class":157},[137,29569,29356],{"class":8180},[137,29571,29509],{"class":147},[137,29573,253],{"class":157},[137,29575,29514],{"class":284},[137,29577,29517],{"class":147},[137,29579,253],{"class":157},[137,29581,5393],{"class":284},[137,29583,4078],{"class":157},[137,29585,29586,29588,29590,29593,29595],{"class":139,"line":269},[137,29587,4072],{"class":157},[137,29589,27],{"class":4036},[137,29591,29592],{"class":157},">Let me know what you think!\u003C\u002F",[137,29594,27],{"class":4036},[137,29596,4053],{"class":157},[137,29598,29599,29601,29603],{"class":139,"line":278},[137,29600,4083],{"class":157},[137,29602,7821],{"class":4036},[137,29604,4053],{"class":157},[104,29606,22292,29608,29611],{"id":29607},"create-a-card-component-by-utilising-various-components",[22,29609,29610],{},"\u003CCard \u002F>"," component by utilising various components.",[27,29613,29614],{},"Now that we have demonstrated how to create a simple JSX component, let's proceed to create a more complex component using both Vue and JSX together.",[27,29616,29617],{},"Before we start building our Card component, let's add Tailwind CSS to our project. This will make it easier to style our components.",[123,29619,29620],{"id":23042},"Add Tailwind CSS.",[27,29622,29623],{},"Adding Tailwind to our Nuxt project is simple. First, we need to install the required dependencies and initialise Tailwind CSS using the following commands:",[128,29625,29627],{"className":8665,"code":29626,"language":8667,"meta":133,"style":133},"yarn add -D tailwindcss postcss autoprefixer\nnpx tailwindcss init\n",[22,29628,29629,29645],{"__ignoreMap":133},[137,29630,29631,29633,29635,29637,29639,29642],{"class":139,"line":140},[137,29632,17263],{"class":147},[137,29634,17266],{"class":284},[137,29636,23060],{"class":364},[137,29638,23075],{"class":284},[137,29640,29641],{"class":284}," postcss",[137,29643,29644],{"class":284}," autoprefixer\n",[137,29646,29647,29649,29651],{"class":139,"line":173},[137,29648,21167],{"class":147},[137,29650,23075],{"class":284},[137,29652,23078],{"class":284},[27,29654,29655,29656,9772],{},"Next, we need to add the following to the ",[22,29657,21711],{},[128,29659,29661],{"className":13299,"code":29660,"language":13301,"meta":133,"style":133},"css: ['~\u002Fassets\u002Fcss\u002Fmain.css'],\npostcss: {\n    plugins: {\n      tailwindcss: {},\n      autoprefixer: {},\n    },\n  },\n",[22,29662,29663,29675,29682,29689,29697,29704,29708],{"__ignoreMap":133},[137,29664,29665,29667,29670,29673],{"class":139,"line":140},[137,29666,23164],{"class":147},[137,29668,29669],{"class":157},": [",[137,29671,29672],{"class":284},"'~\u002Fassets\u002Fcss\u002Fmain.css'",[137,29674,21916],{"class":157},[137,29676,29677,29680],{"class":139,"line":173},[137,29678,29679],{"class":147},"postcss",[137,29681,1819],{"class":157},[137,29683,29684,29687],{"class":139,"line":188},[137,29685,29686],{"class":147},"    plugins",[137,29688,1819],{"class":157},[137,29690,29691,29694],{"class":139,"line":269},[137,29692,29693],{"class":147},"      tailwindcss",[137,29695,29696],{"class":157},": {},\n",[137,29698,29699,29702],{"class":139,"line":278},[137,29700,29701],{"class":147},"      autoprefixer",[137,29703,29696],{"class":157},[137,29705,29706],{"class":139,"line":291},[137,29707,775],{"class":157},[137,29709,29710],{"class":139,"line":297},[137,29711,29712],{"class":157},"  },\n",[27,29714,29715,29716,9772],{},"Then, add the following code to the ",[22,29717,29718],{},"tailwind.config.js",[128,29720,29722],{"className":13299,"code":29721,"language":13301,"meta":133,"style":133},"content: [\"**\u002F*.vue\"],\n",[22,29723,29724],{"__ignoreMap":133},[137,29725,29726,29729,29731,29734],{"class":139,"line":140},[137,29727,29728],{"class":147},"content",[137,29730,29669],{"class":157},[137,29732,29733],{"class":284},"\"**\u002F*.vue\"",[137,29735,21916],{"class":157},[27,29737,29738,29739,29742,29743,29746],{},"Finally, create a ",[22,29740,29741],{},"main.css"," file in the ",[22,29744,29745],{},"assets\u002Fcss"," folder and add the following content:",[128,29748,29749],{"className":23162,"code":23163,"language":23164,"meta":133,"style":133},[22,29750,29751,29757,29763],{"__ignoreMap":133},[137,29752,29753,29755],{"class":139,"line":140},[137,29754,23171],{"class":143},[137,29756,23174],{"class":157},[137,29758,29759,29761],{"class":139,"line":173},[137,29760,23171],{"class":143},[137,29762,23181],{"class":157},[137,29764,29765,29767],{"class":139,"line":188},[137,29766,23171],{"class":143},[137,29768,23188],{"class":157},[123,29770,29772],{"id":29771},"define-the-components","Define the components.",[27,29774,29775,29776],{},"We will build the following Card component:\n",[63,29777],{"alt":29778,"src":29779},"Card Component","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1705811677\u002Fblog\u002Fhow-to-defining-multiple-components-in-a-single-file-in-nuxt\u002Futw0ryorqxndtrcr0lbl",[27,29781,29782],{},"However, before we start building the component, we will divide it into several smaller individual components.",[1003,29784,29785,29794,29800,29806],{},[1006,29786,29787,29790,29791,29793],{},[22,29788,29789],{},"\u003CCardImage \u002F>",": This will be a regular Vue component defined inside the ",[22,29792,21759],{}," directory. We are using a Vue component to demonstrate the ability to mix and match Vue and JSX components.",[1006,29795,29796,29799],{},[22,29797,29798],{},"\u003CCardDescription \u002F>",": This will be a JSX component.",[1006,29801,29802,29805],{},[22,29803,29804],{},"\u003CCardButton \u002F>",": This will also be a JSX component.",[1006,29807,29808,29810],{},[22,29809,29610],{},": This will be the master component, also built with JSX, that will include all of the components mentioned above.",[27,29812,29813],{},"Now that we have listed our components, let's start building them.",[123,29815,29817,29818,29820],{"id":29816},"create-the-cardimage-component","Create the ",[22,29819,29789],{}," component.",[27,29822,29823,29824,29826,29827,29829,29830,9628],{},"First, we are going to create the ",[22,29825,29789],{}," component. As mentioned earlier, this will be a regular Vue component. Inside the ",[22,29828,21759],{}," folder in Nuxt, we are going to create the following ",[22,29831,29832],{},"CardImage.vue",[128,29834,29836],{"className":13299,"code":29835,"language":13301,"meta":133,"style":133},"\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n    src: string;\n}>();\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cimg class=\"mx-auto h-48 w-48 rounded-full md:h-56 md:w-56\" :src=\"props.src\" alt=\"card image\" \u002F>\n\u003C\u002Ftemplate>\n",[22,29837,29838,29851,29866,29877,29882,29890,29894,29902,29931],{"__ignoreMap":133},[137,29839,29840,29842,29844,29846,29849],{"class":139,"line":140},[137,29841,4033],{"class":143},[137,29843,29224],{"class":157},[137,29845,253],{"class":143},[137,29847,29848],{"class":284},"\"ts\"",[137,29850,4053],{"class":143},[137,29852,29853,29855,29858,29860,29863],{"class":139,"line":173},[137,29854,3077],{"class":143},[137,29856,29857],{"class":364}," props",[137,29859,151],{"class":143},[137,29861,29862],{"class":147}," defineProps",[137,29864,29865],{"class":157},"\u003C{\n",[137,29867,29868,29871,29873,29875],{"class":139,"line":188},[137,29869,29870],{"class":161},"    src",[137,29872,894],{"class":143},[137,29874,13630],{"class":364},[137,29876,3276],{"class":157},[137,29878,29879],{"class":139,"line":269},[137,29880,29881],{"class":157},"}>();\n",[137,29883,29884,29886,29888],{"class":139,"line":278},[137,29885,4083],{"class":143},[137,29887,4037],{"class":157},[137,29889,4053],{"class":143},[137,29891,29892],{"class":139,"line":291},[137,29893,516],{"emptyLinePlaceholder":515},[137,29895,29896,29898,29900],{"class":139,"line":297},[137,29897,4033],{"class":157},[137,29899,7821],{"class":147},[137,29901,4053],{"class":157},[137,29903,29904,29906,29909,29911,29914,29917,29919,29922,29924,29926,29929],{"class":139,"line":302},[137,29905,4072],{"class":143},[137,29907,29908],{"class":157},"img class",[137,29910,253],{"class":143},[137,29912,29913],{"class":284},"\"mx-auto h-48 w-48 rounded-full md:h-56 md:w-56\"",[137,29915,29916],{"class":157}," :src",[137,29918,253],{"class":143},[137,29920,29921],{"class":284},"\"props.src\"",[137,29923,10483],{"class":157},[137,29925,253],{"class":143},[137,29927,29928],{"class":284},"\"card image\"",[137,29930,4078],{"class":143},[137,29932,29933,29935,29937],{"class":139,"line":662},[137,29934,4083],{"class":143},[137,29936,7821],{"class":157},[137,29938,4053],{"class":143},[27,29940,29941,29942,29945],{},"Here, we will only have one property, ",[22,29943,29944],{},"src",", where we can add the image path.",[123,29947,29817,29949,29820],{"id":29948},"create-the-carddescription-component",[22,29950,29798],{},[27,29952,29953,29954,29956,29957,29959,29960,29963,29964,29966],{},"Next, we will create a ",[22,29955,29798],{}," JSX component. This component will be declared in the ",[22,29958,21801],{}," component located in the root directory. We will define the script setup with ",[22,29961,29962],{},"lang=\"tsx\"",", as we have shown earlier in the article. To avoid repetition, we will refrain from adding ",[22,29965,29962],{}," to the JSX components that we will declare later in the article.",[128,29968,29970],{"className":13299,"code":29969,"language":13301,"meta":133,"style":133},"const CardDescription = (props: { name: string; title: string }) => (\n    \u003C>\n        \u003Ch3 class=\"mt-6 text-base font-semibold leading-7 tracking-tight text-white\">{props.name}\u003C\u002Fh3>\n        \u003Cp class=\"text-sm leading-6 text-yellow-400\">{props.title}\u003C\u002Fp>\n    \u003C\u002F>\n);\n",[22,29971,29972,30010,30015,30039,30062,30067],{"__ignoreMap":133},[137,29973,29974,29976,29979,29981,29983,29985,29987,29989,29991,29993,29995,29997,29999,30001,30003,30005,30007],{"class":139,"line":140},[137,29975,3077],{"class":143},[137,29977,29978],{"class":147}," CardDescription",[137,29980,151],{"class":143},[137,29982,158],{"class":157},[137,29984,29277],{"class":161},[137,29986,894],{"class":143},[137,29988,8906],{"class":157},[137,29990,1387],{"class":161},[137,29992,894],{"class":143},[137,29994,13630],{"class":364},[137,29996,2323],{"class":157},[137,29998,25683],{"class":161},[137,30000,894],{"class":143},[137,30002,13630],{"class":364},[137,30004,29299],{"class":157},[137,30006,222],{"class":143},[137,30008,30009],{"class":157}," (\n",[137,30011,30012],{"class":139,"line":173},[137,30013,30014],{"class":143},"    \u003C>\n",[137,30016,30017,30019,30022,30024,30027,30030,30033,30035,30037],{"class":139,"line":188},[137,30018,9826],{"class":143},[137,30020,30021],{"class":157},"h3 class",[137,30023,253],{"class":143},[137,30025,30026],{"class":284},"\"mt-6 text-base font-semibold leading-7 tracking-tight text-white\"",[137,30028,30029],{"class":143},">",[137,30031,30032],{"class":157},"{props.name}",[137,30034,4083],{"class":143},[137,30036,123],{"class":157},[137,30038,4053],{"class":143},[137,30040,30041,30043,30046,30048,30051,30053,30056,30058,30060],{"class":139,"line":269},[137,30042,9826],{"class":143},[137,30044,30045],{"class":157},"p class",[137,30047,253],{"class":143},[137,30049,30050],{"class":284},"\"text-sm leading-6 text-yellow-400\"",[137,30052,30029],{"class":143},[137,30054,30055],{"class":157},"{props.title}",[137,30057,4083],{"class":143},[137,30059,27],{"class":157},[137,30061,4053],{"class":143},[137,30063,30064],{"class":139,"line":278},[137,30065,30066],{"class":143},"    \u003C\u002F>\n",[137,30068,30069],{"class":139,"line":291},[137,30070,1502],{"class":157},[27,30072,30073,30074,114,30076,1017],{},"This component will accept two props: ",[22,30075,1387],{},[22,30077,25683],{},[123,30079,29817,30081,29820],{"id":30080},"create-the-cardbutton-component",[22,30082,29804],{},[27,30084,4737,30085,30087,30088,30090,30091,30093,30094,30097,30098,30100,30101,114,30103,30106,30107,30110,30111,894],{},[22,30086,29804],{}," is another JSX component that will be created in the ",[22,30089,21801],{}," file. In this component, we will demonstrate how to use a function declared in the ",[22,30092,9591],{}," section and utilise it inside the ",[22,30095,30096],{},"onClick"," event of this component. The function will simply toggle a boolean reactive ",[22,30099,27815],{}," variable between the ",[22,30102,3097],{},[22,30104,30105],{},"false"," states. Later, we will make use of this reactive variable in our ",[22,30108,30109],{},"\u003CCard\u002F>"," component in the next section. So, let's first declare the function and the reactive variable ",[22,30112,30113],{},"isCardRed",[128,30115,30117],{"className":13299,"code":30116,"language":13301,"meta":133,"style":133},"const isCardRed = ref(true);\n\nfunction updateStyle() {\n    isCardRed.value = !isCardRed.value;\n}\n",[22,30118,30119,30136,30140,30149,30161],{"__ignoreMap":133},[137,30120,30121,30123,30126,30128,30130,30132,30134],{"class":139,"line":140},[137,30122,3077],{"class":143},[137,30124,30125],{"class":364}," isCardRed",[137,30127,151],{"class":143},[137,30129,10468],{"class":147},[137,30131,356],{"class":157},[137,30133,3097],{"class":364},[137,30135,1502],{"class":157},[137,30137,30138],{"class":139,"line":173},[137,30139,516],{"emptyLinePlaceholder":515},[137,30141,30142,30144,30147],{"class":139,"line":188},[137,30143,483],{"class":143},[137,30145,30146],{"class":147}," updateStyle",[137,30148,275],{"class":157},[137,30150,30151,30154,30156,30158],{"class":139,"line":269},[137,30152,30153],{"class":157},"    isCardRed.value ",[137,30155,253],{"class":143},[137,30157,27133],{"class":143},[137,30159,30160],{"class":157},"isCardRed.value;\n",[137,30162,30163],{"class":139,"line":278},[137,30164,510],{"class":157},[27,30166,30167,30168,9628],{},"Next, we will declare the ",[22,30169,29804],{},[128,30171,30173],{"className":13299,"code":30172,"language":13301,"meta":133,"style":133},"const CardButton = (props: { action: () => any }, components: { slots: any }) => (\n    \u003Cdiv class=\"mt-6 flex justify-center gap-x-6\">\n        \u003Cbutton\n            onClick={props.action}\n            type=\"submit\"\n            class=\"flex-none rounded-md bg-yellow-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-yellow-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-yellow-500\"\n        >\n            {components.slots.default()}\n        \u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n);\n",[22,30174,30175,30226,30240,30246,30256,30265,30275,30280,30298,30306,30314],{"__ignoreMap":133},[137,30176,30177,30179,30182,30184,30186,30188,30190,30192,30195,30197,30199,30201,30203,30206,30209,30211,30213,30216,30218,30220,30222,30224],{"class":139,"line":140},[137,30178,3077],{"class":143},[137,30180,30181],{"class":147}," CardButton",[137,30183,151],{"class":143},[137,30185,158],{"class":157},[137,30187,29277],{"class":161},[137,30189,894],{"class":143},[137,30191,8906],{"class":157},[137,30193,30194],{"class":147},"action",[137,30196,894],{"class":143},[137,30198,1484],{"class":157},[137,30200,222],{"class":143},[137,30202,26137],{"class":364},[137,30204,30205],{"class":157}," }, ",[137,30207,30208],{"class":161},"components",[137,30210,894],{"class":143},[137,30212,8906],{"class":157},[137,30214,30215],{"class":161},"slots",[137,30217,894],{"class":143},[137,30219,26137],{"class":364},[137,30221,29299],{"class":157},[137,30223,222],{"class":143},[137,30225,30009],{"class":157},[137,30227,30228,30230,30233,30235,30238],{"class":139,"line":173},[137,30229,4072],{"class":143},[137,30231,30232],{"class":157},"div class",[137,30234,253],{"class":143},[137,30236,30237],{"class":284},"\"mt-6 flex justify-center gap-x-6\"",[137,30239,4053],{"class":143},[137,30241,30242,30244],{"class":139,"line":188},[137,30243,9826],{"class":143},[137,30245,8385],{"class":161},[137,30247,30248,30251,30253],{"class":139,"line":269},[137,30249,30250],{"class":157},"            onClick",[137,30252,253],{"class":143},[137,30254,30255],{"class":157},"{props.action}\n",[137,30257,30258,30261,30263],{"class":139,"line":278},[137,30259,30260],{"class":157},"            type",[137,30262,253],{"class":143},[137,30264,24136],{"class":284},[137,30266,30267,30270,30272],{"class":139,"line":291},[137,30268,30269],{"class":157},"            class",[137,30271,253],{"class":143},[137,30273,30274],{"class":284},"\"flex-none rounded-md bg-yellow-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-yellow-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-yellow-500\"\n",[137,30276,30277],{"class":139,"line":297},[137,30278,30279],{"class":143},"        >\n",[137,30281,30282,30285,30287,30289,30291,30293,30295],{"class":139,"line":302},[137,30283,30284],{"class":157},"            {",[137,30286,30208],{"class":161},[137,30288,1017],{"class":157},[137,30290,30215],{"class":161},[137,30292,1017],{"class":157},[137,30294,9757],{"class":161},[137,30296,30297],{"class":157},"()}\n",[137,30299,30300,30302,30304],{"class":139,"line":662},[137,30301,9843],{"class":143},[137,30303,8170],{"class":157},[137,30305,4053],{"class":143},[137,30307,30308,30310,30312],{"class":139,"line":667},[137,30309,8374],{"class":143},[137,30311,8330],{"class":157},[137,30313,4053],{"class":143},[137,30315,30316],{"class":139,"line":786},[137,30317,1502],{"class":157},[27,30319,30320,30321,30323,30324,30326],{},"As we can see, we only have one prop here called ",[22,30322,30194],{},", which we will use to pass a function. We will pass the ",[22,30325,4361],{}," function to this prop later when we use this component.",[27,30328,30329,30330,30332,30333,30335,30336,30339],{},"Another parameter apart from the props we are passing is the ",[22,30331,30208],{}," parameter, which has a property of ",[22,30334,30215],{},". We call this parameter inside the JSX as ",[22,30337,30338],{},"components.slots.default()",". This allows us to pass slots in the same way as we do with Vue components, something like this:",[128,30341,30343],{"className":4024,"code":30342,"language":4026,"meta":133,"style":133},"\u003CCardButton :action=\"updateStyle\">Switch Colour\u003C\u002FCardButton>\n",[22,30344,30345],{"__ignoreMap":133},[137,30346,30347,30349,30352,30355,30357,30360,30363,30365],{"class":139,"line":140},[137,30348,4033],{"class":157},[137,30350,30351],{"class":8180},"CardButton",[137,30353,30354],{"class":147}," :action",[137,30356,253],{"class":157},[137,30358,30359],{"class":284},"\"updateStyle\"",[137,30361,30362],{"class":157},">Switch Colour\u003C\u002F",[137,30364,30351],{"class":8180},[137,30366,4053],{"class":157},[27,30368,30369,30370,30372],{},"Now, let's build our main ",[22,30371,29610],{}," component where we can use all the components we declared above together.",[123,30374,29817,30376,29820],{"id":30375},"create-the-card-component",[22,30377,29610],{},[27,30379,30380,30381,21802,30383,30385],{},"This will be our last JSX component, also declared in the ",[22,30382,21801],{},[22,30384,9591],{},". It is going to be pretty simple. In this component, we will pass components as slots.",[128,30387,30389],{"className":13299,"code":30388,"language":13301,"meta":133,"style":133},"const Card = (props: { toggleColour: boolean }, components: { slots: any }) => (\n    \u003C>\n        \u003Cdiv class={\"rounded-2xl px-8 py-10 w-80 \" + `${props.toggleColour ? \"bg-red-800\" : \"bg-yellow-800\"}`}>\n            {components.slots.default()}\n        \u003C\u002Fdiv>\n    \u003C\u002F>\n);\n",[22,30390,30391,30435,30439,30479,30495,30503,30507],{"__ignoreMap":133},[137,30392,30393,30395,30398,30400,30402,30404,30406,30408,30411,30413,30415,30417,30419,30421,30423,30425,30427,30429,30431,30433],{"class":139,"line":140},[137,30394,3077],{"class":143},[137,30396,30397],{"class":147}," Card",[137,30399,151],{"class":143},[137,30401,158],{"class":157},[137,30403,29277],{"class":161},[137,30405,894],{"class":143},[137,30407,8906],{"class":157},[137,30409,30410],{"class":161},"toggleColour",[137,30412,894],{"class":143},[137,30414,14110],{"class":364},[137,30416,30205],{"class":157},[137,30418,30208],{"class":161},[137,30420,894],{"class":143},[137,30422,8906],{"class":157},[137,30424,30215],{"class":161},[137,30426,894],{"class":143},[137,30428,26137],{"class":364},[137,30430,29299],{"class":157},[137,30432,222],{"class":143},[137,30434,30009],{"class":157},[137,30436,30437],{"class":139,"line":173},[137,30438,30014],{"class":143},[137,30440,30441,30443,30445,30447,30450,30453,30456,30458,30460,30462,30464,30466,30469,30471,30474,30477],{"class":139,"line":188},[137,30442,9826],{"class":143},[137,30444,30232],{"class":157},[137,30446,253],{"class":143},[137,30448,30449],{"class":157},"{",[137,30451,30452],{"class":284},"\"rounded-2xl px-8 py-10 w-80 \"",[137,30454,30455],{"class":157}," + ",[137,30457,18820],{"class":284},[137,30459,29277],{"class":157},[137,30461,1017],{"class":284},[137,30463,30410],{"class":157},[137,30465,26196],{"class":143},[137,30467,30468],{"class":284}," \"bg-red-800\"",[137,30470,26201],{"class":143},[137,30472,30473],{"class":284}," \"bg-yellow-800\"}`",[137,30475,30476],{"class":157},"}",[137,30478,4053],{"class":143},[137,30480,30481,30483,30485,30487,30489,30491,30493],{"class":139,"line":269},[137,30482,30284],{"class":157},[137,30484,30208],{"class":161},[137,30486,1017],{"class":157},[137,30488,30215],{"class":161},[137,30490,1017],{"class":157},[137,30492,9757],{"class":161},[137,30494,30297],{"class":157},[137,30496,30497,30499,30501],{"class":139,"line":278},[137,30498,9843],{"class":143},[137,30500,8330],{"class":157},[137,30502,4053],{"class":143},[137,30504,30505],{"class":139,"line":291},[137,30506,30066],{"class":143},[137,30508,30509],{"class":139,"line":297},[137,30510,1502],{"class":157},[27,30512,30513,30514,30516,30517,30519,30520,30522,30523,30525,30526,30528,30529,30531],{},"We have a prop called ",[22,30515,30410],{}," which is a boolean. In this prop, we are going to pass the ",[22,30518,30113],{}," reactive ",[22,30521,27815],{}," variable that we declared earlier when we declared the ",[22,30524,29804],{},". We use this ",[22,30527,30410],{}," prop in the ",[22,30530,29610],{}," component to conditionally toggle between two background colours.",[27,30533,30534,30535,30537,30538,30540,30541,30543],{},"Similarly, we are also passing the ",[22,30536,30208],{}," property, just like we did in the ",[22,30539,29804],{},", using ",[22,30542,30338],{},". This allows us to pass the components that we declare above.",[27,30545,30546,30547,30549],{},"Lastly, we can use the ",[22,30548,29610],{}," component together with all the components mentioned above like this:",[128,30551,30553],{"className":4024,"code":30552,"language":4026,"meta":133,"style":133},"\u003Ctemplate>\n    \u003Cdiv class=\"flex justify-center p-4\">\n        \u003CCard :toggleColour=\"isCardRed\">\n            \u003CCardImage\n                src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80\"\n            \u002F>\n            \u003CCardDescription name=\"Lopez Simba\" title=\"Human Resources\" \u002F>\n            \u003CCardButton :action=\"updateStyle\">Switch Colour\u003C\u002FCardButton>\n        \u003C\u002FCard>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,30554,30555,30563,30578,30595,30602,30612,30617,30641,30659,30667,30675],{"__ignoreMap":133},[137,30556,30557,30559,30561],{"class":139,"line":140},[137,30558,4033],{"class":157},[137,30560,7821],{"class":4036},[137,30562,4053],{"class":157},[137,30564,30565,30567,30569,30571,30573,30576],{"class":139,"line":173},[137,30566,4072],{"class":157},[137,30568,8330],{"class":4036},[137,30570,7832],{"class":147},[137,30572,253],{"class":157},[137,30574,30575],{"class":284},"\"flex justify-center p-4\"",[137,30577,4053],{"class":157},[137,30579,30580,30582,30585,30588,30590,30593],{"class":139,"line":188},[137,30581,9826],{"class":157},[137,30583,30584],{"class":8180},"Card",[137,30586,30587],{"class":147}," :toggleColour",[137,30589,253],{"class":157},[137,30591,30592],{"class":284},"\"isCardRed\"",[137,30594,4053],{"class":157},[137,30596,30597,30599],{"class":139,"line":269},[137,30598,23852],{"class":157},[137,30600,30601],{"class":8180},"CardImage\n",[137,30603,30604,30607,30609],{"class":139,"line":278},[137,30605,30606],{"class":147},"                src",[137,30608,253],{"class":157},[137,30610,30611],{"class":284},"\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80\"\n",[137,30613,30614],{"class":139,"line":291},[137,30615,30616],{"class":157},"            \u002F>\n",[137,30618,30619,30621,30624,30626,30628,30631,30634,30636,30639],{"class":139,"line":297},[137,30620,23852],{"class":157},[137,30622,30623],{"class":8180},"CardDescription",[137,30625,891],{"class":147},[137,30627,253],{"class":157},[137,30629,30630],{"class":284},"\"Lopez Simba\"",[137,30632,30633],{"class":147}," title",[137,30635,253],{"class":157},[137,30637,30638],{"class":284},"\"Human Resources\"",[137,30640,4078],{"class":157},[137,30642,30643,30645,30647,30649,30651,30653,30655,30657],{"class":139,"line":302},[137,30644,23852],{"class":157},[137,30646,30351],{"class":8180},[137,30648,30354],{"class":147},[137,30650,253],{"class":157},[137,30652,30359],{"class":284},[137,30654,30362],{"class":157},[137,30656,30351],{"class":8180},[137,30658,4053],{"class":157},[137,30660,30661,30663,30665],{"class":139,"line":662},[137,30662,9843],{"class":157},[137,30664,30584],{"class":8180},[137,30666,4053],{"class":157},[137,30668,30669,30671,30673],{"class":139,"line":667},[137,30670,8374],{"class":157},[137,30672,8330],{"class":4036},[137,30674,4053],{"class":157},[137,30676,30677,30679,30681],{"class":139,"line":786},[137,30678,4083],{"class":157},[137,30680,7821],{"class":4036},[137,30682,4053],{"class":157},[27,30684,30685,30686,30688,30689,30691],{},"And that is it. We now have a fully functional ",[22,30687,29610],{}," using multiple components declared in the same ",[22,30690,7575],{}," file, as well as mixing components between Vue and JSX together while maintaining the same reactivity system.",[27,30693,30694,30695,1017],{},"You can find this example in my GitHub repository at the following ",[45,30696,2726],{"href":30697,"target":2716,"rel":30698},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002FDefining-Multiple-Components-in-a-Single-File-in-Nuxt",[2718,2719],[104,30700,2567],{"id":2566},[27,30702,30703,30704,30706],{},"In this blog article, we have demonstrated how to define multiple components in a single ",[22,30705,7575],{}," file in Nuxt using JSX. By using JSX, we can combine regular Vue components and JSX components in the same file, thus utilising the same reactivity system provided by Vue. While it is generally recommended to separate components into individual files, there are situations where declaring small components in the same file can be beneficial. We hope you found this blog article useful. Please subscribe for more articles like this in the future.",[2617,30708,30709],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":133,"searchDepth":173,"depth":173,"links":30711},[30712,30713,30715,30728],{"id":29051,"depth":173,"text":29052},{"id":29167,"depth":173,"text":30714},"Define our first component in JSX.",{"id":29607,"depth":173,"text":30716,"children":30717},"Create a \u003CCard \u002F> component by utilising various components.",[30718,30719,30720,30722,30724,30726],{"id":23042,"depth":188,"text":29620},{"id":29771,"depth":188,"text":29772},{"id":29816,"depth":188,"text":30721},"Create the \u003CCardImage \u002F> component.",{"id":29948,"depth":188,"text":30723},"Create the \u003CCardDescription \u002F> component.",{"id":30080,"depth":188,"text":30725},"Create the \u003CCardButton \u002F> component.",{"id":30375,"depth":188,"text":30727},"Create the \u003CCard \u002F> component.",{"id":2566,"depth":173,"text":2567},"In this blog article, we have demonstrated how to define multiple components in a single .vue file in Nuxt using JSX. By using JSX, we can combine regular Vue components and JSX components in the same file, thus utilising the same reactivity system provided by Vue. While it is generally recommended to separate components into individual files, there are situations where declaring small components in the same file can be beneficial. We hope you found this blog article useful. Please subscribe for more articles like this in the future.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1705811682\u002Fblog\u002Fhow-to-defining-multiple-components-in-a-single-file-in-nuxt\u002Fdx3lzu2av0hwbu2hkuob",[21096,8,12817,29171,30732,22769,30733,30734,26585,8448,5300,5299],"TSX","Multiple Components","Single File",{},"\u002F2024\u002F01\u002F23\u002Fhow-to-define-multiple-components-in-a-single-file-in-nuxt-using-jsx","23rd January 2024",{"title":29008,"description":30729},"2024\u002F01\u002F23\u002Fhow-to-define-multiple-components-in-a-single-file-in-nuxt-using-jsx","4kc-nGfo0Rn8l5IvY2hhbN-qBBBwx1DZdhAhiNOG8n4",{"id":30742,"title":30743,"articleTags":30744,"author":11,"blog":12,"body":30745,"description":33545,"extension":2649,"image":33546,"keywords":33547,"meta":33554,"navigation":515,"path":33555,"published":33556,"readTime":798,"seo":33557,"stem":33558,"type":2662,"__hash__":33559},"content\u002F2024\u002F02\u002F25\u002Fcreate-a-web-server-using-nitro-sqlite-and-typeorm.md","Create a Web Server using Nitro, SQLite and TypeORM",[21134,22224,12817],{"type":14,"value":30746,"toc":33528},[30747,30750,30764,30766,30770,30775,30783,30786,30790,30796,30814,30820,30823,30846,30852,30875,30878,30882,30891,30911,30936,30949,31107,31110,31119,31238,31253,31264,31294,31298,31307,31319,31332,31470,31486,31490,31493,31511,31524,31527,31541,31544,31558,31570,31573,31577,31580,31590,31724,31730,31734,31751,31759,31770,31892,31895,31898,31902,31911,31914,31940,31943,31947,31958,31969,32147,32150,32160,32220,32229,32236,32239,32297,32301,32307,32396,32405,32446,32449,32453,32458,32562,32588,32655,32660,32663,32667,32670,32683,32689,32703,32714,33058,33071,33085,33091,33174,33184,33190,33308,33311,33424,33428,33435,33442,33499,33510,33512,33515,33518,33525],[17,30748,30743],{"id":30749},"create-a-web-server-using-nitro-sqlite-and-typeorm",[27,30751,30752],{},[30,30753,30754,36,30756,40,30758],{},[33,30755],{"value":35},[33,30757],{"value":39},[42,30759,30760],{},[45,30761,30762],{"href":47},[33,30763],{"value":50},[52,30765],{":tags":54},[56,30767],{":audio-src":30768,":transcript-src":30769},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2024\u002F02\u002F25\u002Fcreate-a-web-server-using-nitro-sqlite-and-typeorm\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2024\u002F02\u002F25\u002Fcreate-a-web-server-using-nitro-sqlite-and-typeorm\u002Fsummary.json",[27,30771,30772],{},[63,30773],{"alt":12847,"src":30774},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1708844513\u002Fblog\u002Fcreate-a-web-server-using-nitro-sqlite-and-typeorm\u002Fcreate-a-web-server-using-nitro-sqlite-and-typeorm_ozdamt",[27,30776,30777,30778,30782],{},"If you're familiar with ",[45,30779,21096],{"href":30780,"target":2716,"rel":30781},"https:\u002F\u002Fnuxt.com\u002F",[2718,2719],", you might know that the latest version, Nuxt version 3, runs on a new server engine called Nitro. Nitro isn't just used in Nuxt, it's also an independent open-source framework for developing web server applications. It provides several built-in features that make it a modern, user-friendly backend framework. Nitro is open-source and maintained by the same core team as Nuxt.js.",[27,30784,30785],{},"In this blog post, we'll show you how to build a simple web server using Nitro and connect it to an SQLite database. We'll also explain the process of database migration with TypeORM, a popular typescript library, introduce the widely-used pattern for validating request data, the Data Transfer Object (DTO), and utilise some of Nitro's built-in features to cache results.",[104,30787,30789],{"id":30788},"create-a-nitro-web-server","Create a Nitro Web Server",[27,30791,30792,30793,1017],{},"First, we need to create a new project using the following command in the terminal of your choice. We will name our project ",[22,30794,30795],{},"nitro-sqlite",[128,30797,30799],{"className":8665,"code":30798,"language":8667,"meta":133,"style":133},"npx giget@latest nitro nitro-sqlite\n",[22,30800,30801],{"__ignoreMap":133},[137,30802,30803,30805,30808,30811],{"class":139,"line":140},[137,30804,21167],{"class":147},[137,30806,30807],{"class":284}," giget@latest",[137,30809,30810],{"class":284}," nitro",[137,30812,30813],{"class":284}," nitro-sqlite\n",[27,30815,30816,30817,30819],{},"In this article, we're going to use ",[22,30818,17263],{}," as a package management tool, but feel free to use your preferred one.",[27,30821,30822],{},"We will need to install the dependencies and finally run the development server:",[128,30824,30826],{"className":8665,"code":30825,"language":8667,"meta":133,"style":133},"cd nitro-sqlite\nyarn install\nyarn dev\n",[22,30827,30828,30834,30840],{"__ignoreMap":133},[137,30829,30830,30832],{"class":139,"line":140},[137,30831,9558],{"class":364},[137,30833,30813],{"class":284},[137,30835,30836,30838],{"class":139,"line":173},[137,30837,17263],{"class":147},[137,30839,9571],{"class":284},[137,30841,30842,30844],{"class":139,"line":188},[137,30843,17263],{"class":147},[137,30845,9581],{"class":284},[27,30847,30848,30849,30851],{},"If you open your browser and navigate to ",[22,30850,13068],{},", you will see the following:",[128,30853,30855],{"className":5155,"code":30854,"language":5157,"meta":133,"style":133},"{\n    \"nitro\": \"Is Awesome!\"\n}\n",[22,30856,30857,30861,30871],{"__ignoreMap":133},[137,30858,30859],{"class":139,"line":140},[137,30860,15971],{"class":157},[137,30862,30863,30866,30868],{"class":139,"line":173},[137,30864,30865],{"class":364},"    \"nitro\"",[137,30867,726],{"class":157},[137,30869,30870],{"class":284},"\"Is Awesome!\"\n",[137,30872,30873],{"class":139,"line":188},[137,30874,510],{"class":157},[27,30876,30877],{},"Great, we've successfully set up our Nitro web server.",[104,30879,30881],{"id":30880},"database-setup","Database Setup",[27,30883,30884,30885,30890],{},"In this project, we'll be using SQLite, but the same setup can be applied to any database. We'll establish the database using a popular Object-Relational Mapping (ORM) library in TypeScript, ",[45,30886,30889],{"href":30887,"target":2716,"rel":30888},"https:\u002F\u002Ftypeorm.io\u002F",[2718,2719],"TypeORM",". TypeORM simplifies the creation and querying of data from a relational database using an object-oriented approach, assists in defining the database schema, and manages migrations. We'll explore these features later. For now, let's install the required libraries:",[128,30892,30894],{"className":8665,"code":30893,"language":8667,"meta":133,"style":133},"yarn add reflect-metadata sqlite3 typeorm\n",[22,30895,30896],{"__ignoreMap":133},[137,30897,30898,30900,30902,30905,30908],{"class":139,"line":140},[137,30899,17263],{"class":147},[137,30901,17266],{"class":284},[137,30903,30904],{"class":284}," reflect-metadata",[137,30906,30907],{"class":284}," sqlite3",[137,30909,30910],{"class":284}," typeorm\n",[128,30912,30914],{"className":8665,"code":30913,"language":8667,"meta":133,"style":133},"yarn add -D ts-node @types\u002Fnode typescript rollup-plugin-typescript2\n",[22,30915,30916],{"__ignoreMap":133},[137,30917,30918,30920,30922,30924,30927,30930,30933],{"class":139,"line":140},[137,30919,17263],{"class":147},[137,30921,17266],{"class":284},[137,30923,23060],{"class":364},[137,30925,30926],{"class":284}," ts-node",[137,30928,30929],{"class":284}," @types\u002Fnode",[137,30931,30932],{"class":284}," typescript",[137,30934,30935],{"class":284}," rollup-plugin-typescript2\n",[27,30937,23191,30938,30941,30942,30945,30946,30948],{},[22,30939,30940],{},"nitro.config.ts",", import the rollup plugin ",[22,30943,30944],{},"rollup-plugin-typescript2"," and adjust the TypeScript compiler options before proceeding with the database setup. Ensure that your ",[22,30947,30940],{}," looks like the following:",[128,30950,30952],{"className":13299,"code":30951,"language":13301,"meta":133,"style":133},"import typescript from \"rollup-plugin-typescript2\";\n\nexport default defineNitroConfig({\n    rollupConfig: {\n        plugins: [typescript()],\n    },\n    typescript: {\n        tsConfig: {\n            compilerOptions: {\n                lib: [\"es2021\"],\n                target: \"es2021\",\n                module: \"es2022\",\n                moduleResolution: \"node\",\n                allowSyntheticDefaultImports: true,\n                emitDecoratorMetadata: true,\n                experimentalDecorators: true,\n                sourceMap: true,\n            },\n        },\n    },\n});\n",[22,30953,30954,30968,30972,30983,30988,30997,31001,31006,31011,31016,31026,31035,31045,31055,31064,31073,31082,31091,31095,31099,31103],{"__ignoreMap":133},[137,30955,30956,30958,30961,30963,30966],{"class":139,"line":140},[137,30957,10287],{"class":143},[137,30959,30960],{"class":157}," typescript ",[137,30962,10954],{"class":143},[137,30964,30965],{"class":284}," \"rollup-plugin-typescript2\"",[137,30967,3276],{"class":157},[137,30969,30970],{"class":139,"line":173},[137,30971,516],{"emptyLinePlaceholder":515},[137,30973,30974,30976,30978,30981],{"class":139,"line":188},[137,30975,13456],{"class":143},[137,30977,21723],{"class":143},[137,30979,30980],{"class":147}," defineNitroConfig",[137,30982,3175],{"class":157},[137,30984,30985],{"class":139,"line":269},[137,30986,30987],{"class":157},"    rollupConfig: {\n",[137,30989,30990,30993,30995],{"class":139,"line":278},[137,30991,30992],{"class":157},"        plugins: [",[137,30994,27873],{"class":147},[137,30996,23025],{"class":157},[137,30998,30999],{"class":139,"line":291},[137,31000,775],{"class":157},[137,31002,31003],{"class":139,"line":297},[137,31004,31005],{"class":157},"    typescript: {\n",[137,31007,31008],{"class":139,"line":302},[137,31009,31010],{"class":157},"        tsConfig: {\n",[137,31012,31013],{"class":139,"line":662},[137,31014,31015],{"class":157},"            compilerOptions: {\n",[137,31017,31018,31021,31024],{"class":139,"line":667},[137,31019,31020],{"class":157},"                lib: [",[137,31022,31023],{"class":284},"\"es2021\"",[137,31025,21916],{"class":157},[137,31027,31028,31031,31033],{"class":139,"line":786},[137,31029,31030],{"class":157},"                target: ",[137,31032,31023],{"class":284},[137,31034,1961],{"class":157},[137,31036,31037,31040,31043],{"class":139,"line":798},[137,31038,31039],{"class":157},"                module: ",[137,31041,31042],{"class":284},"\"es2022\"",[137,31044,1961],{"class":157},[137,31046,31047,31050,31053],{"class":139,"line":803},[137,31048,31049],{"class":157},"                moduleResolution: ",[137,31051,31052],{"class":284},"\"node\"",[137,31054,1961],{"class":157},[137,31056,31057,31060,31062],{"class":139,"line":931},[137,31058,31059],{"class":157},"                allowSyntheticDefaultImports: ",[137,31061,3097],{"class":364},[137,31063,1961],{"class":157},[137,31065,31066,31069,31071],{"class":139,"line":1568},[137,31067,31068],{"class":157},"                emitDecoratorMetadata: ",[137,31070,3097],{"class":364},[137,31072,1961],{"class":157},[137,31074,31075,31078,31080],{"class":139,"line":1573},[137,31076,31077],{"class":157},"                experimentalDecorators: ",[137,31079,3097],{"class":364},[137,31081,1961],{"class":157},[137,31083,31084,31087,31089],{"class":139,"line":1578},[137,31085,31086],{"class":157},"                sourceMap: ",[137,31088,3097],{"class":364},[137,31090,1961],{"class":157},[137,31092,31093],{"class":139,"line":1588},[137,31094,14074],{"class":157},[137,31096,31097],{"class":139,"line":1601},[137,31098,2084],{"class":157},[137,31100,31101],{"class":139,"line":3802},[137,31102,775],{"class":157},[137,31104,31105],{"class":139,"line":3808},[137,31106,5422],{"class":157},[27,31108,31109],{},"Without the above settings, I encountered several compiler errors with the TypeORM setup, so it's necessary to ensure the compiler options are set as described above.",[27,31111,31112,31113,31116,31117,14996],{},"Next, we will create a file named ",[22,31114,31115],{},"ormconfig.ts"," in the root of the project. This ORM configuration will be used when we create our migrations. The content of the ",[22,31118,31115],{},[128,31120,31122],{"className":13299,"code":31121,"language":13301,"meta":133,"style":133},"import { DataSource } from \"typeorm\";\n\nconst dataSource = new DataSource({\n    type: \"sqlite\",\n    database: \"database.sqlite\",\n    synchronize: false,\n    logging: false,\n    subscribers: [],\n    entities: [\"entity\u002F**\u002F*{.js,.ts}\"],\n    migrations: [\"migrations\u002F**\u002F*{.js,.ts}\"],\n});\n\nexport default dataSource;\n",[22,31123,31124,31138,31142,31158,31168,31178,31187,31196,31201,31211,31221,31225,31229],{"__ignoreMap":133},[137,31125,31126,31128,31131,31133,31136],{"class":139,"line":140},[137,31127,10287],{"class":143},[137,31129,31130],{"class":157}," { DataSource } ",[137,31132,10954],{"class":143},[137,31134,31135],{"class":284}," \"typeorm\"",[137,31137,3276],{"class":157},[137,31139,31140],{"class":139,"line":173},[137,31141,516],{"emptyLinePlaceholder":515},[137,31143,31144,31146,31149,31151,31153,31156],{"class":139,"line":188},[137,31145,3077],{"class":143},[137,31147,31148],{"class":364}," dataSource",[137,31150,151],{"class":143},[137,31152,1426],{"class":143},[137,31154,31155],{"class":147}," DataSource",[137,31157,3175],{"class":157},[137,31159,31160,31163,31166],{"class":139,"line":269},[137,31161,31162],{"class":157},"    type: ",[137,31164,31165],{"class":284},"\"sqlite\"",[137,31167,1961],{"class":157},[137,31169,31170,31173,31176],{"class":139,"line":278},[137,31171,31172],{"class":157},"    database: ",[137,31174,31175],{"class":284},"\"database.sqlite\"",[137,31177,1961],{"class":157},[137,31179,31180,31183,31185],{"class":139,"line":291},[137,31181,31182],{"class":157},"    synchronize: ",[137,31184,30105],{"class":364},[137,31186,1961],{"class":157},[137,31188,31189,31192,31194],{"class":139,"line":297},[137,31190,31191],{"class":157},"    logging: ",[137,31193,30105],{"class":364},[137,31195,1961],{"class":157},[137,31197,31198],{"class":139,"line":302},[137,31199,31200],{"class":157},"    subscribers: [],\n",[137,31202,31203,31206,31209],{"class":139,"line":662},[137,31204,31205],{"class":157},"    entities: [",[137,31207,31208],{"class":284},"\"entity\u002F**\u002F*{.js,.ts}\"",[137,31210,21916],{"class":157},[137,31212,31213,31216,31219],{"class":139,"line":667},[137,31214,31215],{"class":157},"    migrations: [",[137,31217,31218],{"class":284},"\"migrations\u002F**\u002F*{.js,.ts}\"",[137,31220,21916],{"class":157},[137,31222,31223],{"class":139,"line":786},[137,31224,5422],{"class":157},[137,31226,31227],{"class":139,"line":798},[137,31228,516],{"emptyLinePlaceholder":515},[137,31230,31231,31233,31235],{"class":139,"line":803},[137,31232,13456],{"class":143},[137,31234,21723],{"class":143},[137,31236,31237],{"class":157}," dataSource;\n",[27,31239,31240,31241,31244,31245,31248,31249,31252],{},"In the configuration settings, we named our database ",[22,31242,31243],{},"database.sqlite",", but the name is entirely optional. We also set up the entities (the database schema we are going to create) to be found in the ",[22,31246,31247],{},"\u002Fentity"," directory, and the migrations we will create to be stored in the ",[22,31250,31251],{},"\u002Fmigrations"," directory at the root of the project.",[27,31254,31255,31256,114,31258,31260,31261,31263],{},"Next, we will create the following directories at the root of the project: ",[22,31257,31247],{},[22,31259,31251],{},". And also add the following line in ",[22,31262,5140],{}," in the script section:",[128,31265,31267],{"className":5155,"code":31266,"language":5157,"meta":133,"style":133},"\"scripts\": {\n        ...\n    \"typeorm\": \"typeorm-ts-node-esm -d ormconfig.ts\"\n },\n",[22,31268,31269,31275,31279,31289],{"__ignoreMap":133},[137,31270,31271,31273],{"class":139,"line":140},[137,31272,5164],{"class":284},[137,31274,1819],{"class":157},[137,31276,31277],{"class":139,"line":173},[137,31278,21277],{"class":8180},[137,31280,31281,31284,31286],{"class":139,"line":188},[137,31282,31283],{"class":364},"    \"typeorm\"",[137,31285,726],{"class":157},[137,31287,31288],{"class":284},"\"typeorm-ts-node-esm -d ormconfig.ts\"\n",[137,31290,31291],{"class":139,"line":269},[137,31292,31293],{"class":157}," },\n",[123,31295,31297],{"id":31296},"defining-the-user-entity","Defining the User entity",[27,31299,31300,31301,164,31304,10639],{},"TypeORM utilises TypeScript decorators to define entities and relationships directly within your TypeScript classes. Models can be defined using classes with TypeScript decorators for properties and relationships such as ",[22,31302,31303],{},"@Entity",[22,31305,31306],{},"@Column",[27,31308,31309,31310,31314,31315,31318],{},"For a comprehensive understanding of all decorators, refer to the TypeORM official ",[45,31311,31313],{"href":30887,"target":2716,"rel":31312},[2718,2719],"documentation",". In this blog, we will only demonstrate how to model a simple ",[22,31316,31317],{},"User"," table.",[27,31320,31321,31322,31324,31325,31327,31328,31331],{},"Let's create our User entity within the ",[22,31323,31247],{}," directory. In the ",[22,31326,31247],{}," directory, we will define a file named ",[22,31329,31330],{},"User.ts"," with the following content:",[128,31333,31335],{"className":13299,"code":31334,"language":13301,"meta":133,"style":133},"import { Entity, PrimaryGeneratedColumn, Column } from \"typeorm\";\n\n@Entity(\"user\")\nexport class User {\n    @PrimaryGeneratedColumn()\n    id: number;\n\n    @Column()\n    firstName: string;\n\n    @Column()\n    lastName: string;\n\n    @Column()\n    age: number;\n}\n",[22,31336,31337,31350,31354,31367,31377,31386,31398,31402,31411,31421,31425,31433,31443,31447,31455,31466],{"__ignoreMap":133},[137,31338,31339,31341,31344,31346,31348],{"class":139,"line":140},[137,31340,10287],{"class":143},[137,31342,31343],{"class":157}," { Entity, PrimaryGeneratedColumn, Column } ",[137,31345,10954],{"class":143},[137,31347,31135],{"class":284},[137,31349,3276],{"class":157},[137,31351,31352],{"class":139,"line":173},[137,31353,516],{"emptyLinePlaceholder":515},[137,31355,31356,31358,31361,31363,31365],{"class":139,"line":188},[137,31357,13382],{"class":157},[137,31359,31360],{"class":147},"Entity",[137,31362,356],{"class":157},[137,31364,15823],{"class":284},[137,31366,3155],{"class":157},[137,31368,31369,31371,31373,31375],{"class":139,"line":269},[137,31370,13456],{"class":143},[137,31372,7832],{"class":143},[137,31374,24658],{"class":147},[137,31376,256],{"class":157},[137,31378,31379,31381,31384],{"class":139,"line":278},[137,31380,14664],{"class":157},[137,31382,31383],{"class":147},"PrimaryGeneratedColumn",[137,31385,2754],{"class":157},[137,31387,31388,31391,31393,31396],{"class":139,"line":291},[137,31389,31390],{"class":161},"    id",[137,31392,894],{"class":143},[137,31394,31395],{"class":364}," number",[137,31397,3276],{"class":157},[137,31399,31400],{"class":139,"line":297},[137,31401,516],{"emptyLinePlaceholder":515},[137,31403,31404,31406,31409],{"class":139,"line":302},[137,31405,14664],{"class":157},[137,31407,31408],{"class":147},"Column",[137,31410,2754],{"class":157},[137,31412,31413,31415,31417,31419],{"class":139,"line":662},[137,31414,14868],{"class":161},[137,31416,894],{"class":143},[137,31418,13630],{"class":364},[137,31420,3276],{"class":157},[137,31422,31423],{"class":139,"line":667},[137,31424,516],{"emptyLinePlaceholder":515},[137,31426,31427,31429,31431],{"class":139,"line":786},[137,31428,14664],{"class":157},[137,31430,31408],{"class":147},[137,31432,2754],{"class":157},[137,31434,31435,31437,31439,31441],{"class":139,"line":798},[137,31436,14923],{"class":161},[137,31438,894],{"class":143},[137,31440,13630],{"class":364},[137,31442,3276],{"class":157},[137,31444,31445],{"class":139,"line":803},[137,31446,516],{"emptyLinePlaceholder":515},[137,31448,31449,31451,31453],{"class":139,"line":931},[137,31450,14664],{"class":157},[137,31452,31408],{"class":147},[137,31454,2754],{"class":157},[137,31456,31457,31460,31462,31464],{"class":139,"line":1568},[137,31458,31459],{"class":161},"    age",[137,31461,894],{"class":143},[137,31463,31395],{"class":364},[137,31465,3276],{"class":157},[137,31467,31468],{"class":139,"line":1573},[137,31469,510],{"class":157},[27,31471,31472,31473,31475,31476,164,31479,3596,31482,14528,31484,1017],{},"The above code defines a simple database table named ",[22,31474,13193],{}," with four columns: ",[22,31477,31478],{},"id",[22,31480,31481],{},"firstName,",[22,31483,4703],{},[22,31485,1051],{},[123,31487,31489],{"id":31488},"create-a-migration-for-the-user-table","Create a migration for the User table",[27,31491,31492],{},"Next, we need to create a migration so the User entity will be translated into a database table. To do this, run the following command in your terminal of choice:",[128,31494,31496],{"className":8665,"code":31495,"language":8667,"meta":133,"style":133},"yarn typeorm migration:generate --name=create-user-table\n\n",[22,31497,31498],{"__ignoreMap":133},[137,31499,31500,31502,31505,31508],{"class":139,"line":140},[137,31501,17263],{"class":147},[137,31503,31504],{"class":284}," typeorm",[137,31506,31507],{"class":284}," migration:generate",[137,31509,31510],{"class":364}," --name=create-user-table\n",[27,31512,31513,31514,31517,31518,31520,31521,1017],{},"We name our migration ",[22,31515,31516],{},"create-user-table",". This creates a migration inside the ",[22,31519,31251],{}," directory. Each migration's name will be created with a timestamp followed by the name of the migration. In my case, the migration file was called: ",[22,31522,31523],{},"1708245318068-create-user-table.ts",[27,31525,31526],{},"The migrations can then be run or reverted with the following terminal commands:",[128,31528,31530],{"className":8665,"code":31529,"language":8667,"meta":133,"style":133},"yarn typeorm migration:run\n\n",[22,31531,31532],{"__ignoreMap":133},[137,31533,31534,31536,31538],{"class":139,"line":140},[137,31535,17263],{"class":147},[137,31537,31504],{"class":284},[137,31539,31540],{"class":284}," migration:run\n",[27,31542,31543],{},"or",[128,31545,31547],{"className":8665,"code":31546,"language":8667,"meta":133,"style":133},"yarn typeorm migration:revert\n\n",[22,31548,31549],{"__ignoreMap":133},[137,31550,31551,31553,31555],{"class":139,"line":140},[137,31552,17263],{"class":147},[137,31554,31504],{"class":284},[137,31556,31557],{"class":284}," migration:revert\n",[27,31559,31560,31561,31563,31564,31566,31567,31318],{},"Once the migration is run, the ",[22,31562,31243],{}," database will have two tables: the ",[22,31565,13193],{}," table and the ",[22,31568,31569],{},"migrations",[27,31571,31572],{},"Now we have a database set up, but it's empty. We need to define a couple of endpoints in Nitro so we can add and read users from the database. But before that, we need to configure one more TypeORM setting. Let's do that first.",[123,31574,31576],{"id":31575},"typeorm-configuration","TypeORM Configuration",[27,31578,31579],{},"To use TypeORM for creating and querying users in the database, we need to configure it. This enables us to use the entities and migrations we create.",[27,31581,22292,31582,31585,31586,31589],{},[22,31583,31584],{},"\u002Fconfig"," directory in the root of the project. Inside this directory, create a file named ",[22,31587,31588],{},"config\u002Ftypeorm.config.ts"," with the following configuration:",[128,31591,31593],{"className":13299,"code":31592,"language":13301,"meta":133,"style":133},"import \"reflect-metadata\";\nimport { DataSource } from \"typeorm\";\nimport { User } from \"..\u002Fentity\u002FUser\";\nimport { CreateUserTable1708227011732 } from \"..\u002Fmigrations\u002F1708227011732-create-user-table\";\n\nconst dataSource = new DataSource({\n    type: \"sqlite\",\n    database: \"database.sqlite\",\n    synchronize: false,\n    logging: false,\n    subscribers: [],\n    entities: [User],\n    migrations: [CreateUserTable1708227011732],\n});\n\nexport default dataSource;\n",[22,31594,31595,31604,31616,31630,31644,31648,31662,31670,31678,31686,31694,31698,31703,31708,31712,31716],{"__ignoreMap":133},[137,31596,31597,31599,31602],{"class":139,"line":140},[137,31598,10287],{"class":143},[137,31600,31601],{"class":284}," \"reflect-metadata\"",[137,31603,3276],{"class":157},[137,31605,31606,31608,31610,31612,31614],{"class":139,"line":173},[137,31607,10287],{"class":143},[137,31609,31130],{"class":157},[137,31611,10954],{"class":143},[137,31613,31135],{"class":284},[137,31615,3276],{"class":157},[137,31617,31618,31620,31623,31625,31628],{"class":139,"line":188},[137,31619,10287],{"class":143},[137,31621,31622],{"class":157}," { User } ",[137,31624,10954],{"class":143},[137,31626,31627],{"class":284}," \"..\u002Fentity\u002FUser\"",[137,31629,3276],{"class":157},[137,31631,31632,31634,31637,31639,31642],{"class":139,"line":269},[137,31633,10287],{"class":143},[137,31635,31636],{"class":157}," { CreateUserTable1708227011732 } ",[137,31638,10954],{"class":143},[137,31640,31641],{"class":284}," \"..\u002Fmigrations\u002F1708227011732-create-user-table\"",[137,31643,3276],{"class":157},[137,31645,31646],{"class":139,"line":278},[137,31647,516],{"emptyLinePlaceholder":515},[137,31649,31650,31652,31654,31656,31658,31660],{"class":139,"line":291},[137,31651,3077],{"class":143},[137,31653,31148],{"class":364},[137,31655,151],{"class":143},[137,31657,1426],{"class":143},[137,31659,31155],{"class":147},[137,31661,3175],{"class":157},[137,31663,31664,31666,31668],{"class":139,"line":297},[137,31665,31162],{"class":157},[137,31667,31165],{"class":284},[137,31669,1961],{"class":157},[137,31671,31672,31674,31676],{"class":139,"line":302},[137,31673,31172],{"class":157},[137,31675,31175],{"class":284},[137,31677,1961],{"class":157},[137,31679,31680,31682,31684],{"class":139,"line":662},[137,31681,31182],{"class":157},[137,31683,30105],{"class":364},[137,31685,1961],{"class":157},[137,31687,31688,31690,31692],{"class":139,"line":667},[137,31689,31191],{"class":157},[137,31691,30105],{"class":364},[137,31693,1961],{"class":157},[137,31695,31696],{"class":139,"line":786},[137,31697,31200],{"class":157},[137,31699,31700],{"class":139,"line":798},[137,31701,31702],{"class":157},"    entities: [User],\n",[137,31704,31705],{"class":139,"line":803},[137,31706,31707],{"class":157},"    migrations: [CreateUserTable1708227011732],\n",[137,31709,31710],{"class":139,"line":931},[137,31711,5422],{"class":157},[137,31713,31714],{"class":139,"line":1568},[137,31715,516],{"emptyLinePlaceholder":515},[137,31717,31718,31720,31722],{"class":139,"line":1573},[137,31719,13456],{"class":143},[137,31721,21723],{"class":143},[137,31723,31237],{"class":157},[27,31725,31726,31727,31729],{},"This will be very similar to the ",[22,31728,31115],{}," file, except it imports the User entity and the migrations and adding them to the entities and migrations arrays.",[123,31731,31733],{"id":31732},"initialising-the-database-with-nitro-plugins","Initialising the database with Nitro Plugins",[27,31735,31736,31737,164,31739,31742,31743,31746,31747,1017],{},"Nitro uses a plugin system to extend its runtime behaviour. This is quite useful if you want to add custom behaviour to the web server. Nitro provides several hooks such as on ",[22,31738,19640],{},[22,31740,31741],{},"beforeResponse",", on application ",[22,31744,31745],{},"close",", etc. For more details, refer to the Nitro documentation ",[45,31748,10647],{"href":31749,"target":2716,"rel":31750},"https:\u002F\u002Fnitro.unjs.io\u002Fguide\u002Fplugins#available-hooks",[2718,2719],[27,31752,31753,31754,114,31756,31758],{},"In our case, we will utilise on ",[22,31755,19640],{},[22,31757,31741],{}," hooks to initialise and destroy the database respectively.",[27,31760,31761,31762,31765,31766,31769],{},"To achieve this, create a ",[22,31763,31764],{},"\u002Fplugins"," directory in the root of the project, and then create the following file: ",[22,31767,31768],{},"\u002Fplugins\u002Finitializing-database.ts",". Add the following code to this file:",[128,31771,31773],{"className":13299,"code":31772,"language":13301,"meta":133,"style":133},"import dataSource from \"..\u002Fconfig\u002Ftypeorm.config\";\n\nexport default defineNitroPlugin((nitro) => {\n    nitro.hooks.hook(\"request\", async () => {\n        await dataSource.initialize();\n    });\n    nitro.hooks.hook(\"beforeResponse\", async () => {\n        await dataSource.destroy();\n    });\n});\n",[22,31774,31775,31789,31793,31813,31836,31848,31852,31873,31884,31888],{"__ignoreMap":133},[137,31776,31777,31779,31782,31784,31787],{"class":139,"line":140},[137,31778,10287],{"class":143},[137,31780,31781],{"class":157}," dataSource ",[137,31783,10954],{"class":143},[137,31785,31786],{"class":284}," \"..\u002Fconfig\u002Ftypeorm.config\"",[137,31788,3276],{"class":157},[137,31790,31791],{"class":139,"line":173},[137,31792,516],{"emptyLinePlaceholder":515},[137,31794,31795,31797,31799,31802,31804,31807,31809,31811],{"class":139,"line":188},[137,31796,13456],{"class":143},[137,31798,21723],{"class":143},[137,31800,31801],{"class":147}," defineNitroPlugin",[137,31803,2774],{"class":157},[137,31805,31806],{"class":161},"nitro",[137,31808,219],{"class":157},[137,31810,222],{"class":143},[137,31812,256],{"class":157},[137,31814,31815,31818,31821,31823,31826,31828,31830,31832,31834],{"class":139,"line":269},[137,31816,31817],{"class":157},"    nitro.hooks.",[137,31819,31820],{"class":147},"hook",[137,31822,356],{"class":157},[137,31824,31825],{"class":284},"\"request\"",[137,31827,164],{"class":157},[137,31829,15050],{"class":143},[137,31831,1484],{"class":157},[137,31833,222],{"class":143},[137,31835,256],{"class":157},[137,31837,31838,31840,31843,31846],{"class":139,"line":278},[137,31839,25043],{"class":143},[137,31841,31842],{"class":157}," dataSource.",[137,31844,31845],{"class":147},"initialize",[137,31847,924],{"class":157},[137,31849,31850],{"class":139,"line":291},[137,31851,2832],{"class":157},[137,31853,31854,31856,31858,31860,31863,31865,31867,31869,31871],{"class":139,"line":297},[137,31855,31817],{"class":157},[137,31857,31820],{"class":147},[137,31859,356],{"class":157},[137,31861,31862],{"class":284},"\"beforeResponse\"",[137,31864,164],{"class":157},[137,31866,15050],{"class":143},[137,31868,1484],{"class":157},[137,31870,222],{"class":143},[137,31872,256],{"class":157},[137,31874,31875,31877,31879,31882],{"class":139,"line":302},[137,31876,25043],{"class":143},[137,31878,31842],{"class":157},[137,31880,31881],{"class":147},"destroy",[137,31883,924],{"class":157},[137,31885,31886],{"class":139,"line":662},[137,31887,2832],{"class":157},[137,31889,31890],{"class":139,"line":667},[137,31891,5422],{"class":157},[27,31893,31894],{},"As long as the plugin is created inside the plugins directory, Nitro will automatically import the plugin, so no additional action is required.",[27,31896,31897],{},"We now have everything we need for the database setup. Next, we're going to create the user endpoint to create and read users from the database.",[104,31899,31901],{"id":31900},"user-endpoints","User endpoints",[27,31903,31904,31905,3955,31908,23154],{},"Nitro supports file-based routing for your API routes. You can define a route simply by creating a file inside the ",[22,31906,31907],{},"api\u002F",[22,31909,31910],{},"routes\u002F",[27,31912,31913],{},"In this project, we will define three routes:",[1003,31915,31916,31924,31932],{},[1006,31917,31918,31920,31921],{},[22,31919,22316],{}," route to create users: ",[22,31922,31923],{},"\u002Fcreate-user",[1006,31925,31926,31928,31929],{},[22,31927,22312],{}," route to retrieve all users: ",[22,31930,31931],{},"\u002Fget-users",[1006,31933,31934,31936,31937],{},[22,31935,22312],{}," route to retrieve a single user: ",[22,31938,31939],{},"\u002Fget-user\u002F:id",[27,31941,31942],{},"Let's begin with the first route for creating users.",[123,31944,31946],{"id":31945},"create-a-user-endpoint","Create a user endpoint",[27,31948,31949,31950,31953,31954,31957],{},"In Nitro, we can add specific utils inside the ",[22,31951,31952],{},"utils\u002F"," directory, and they will be automatically imported when used. Every export in the ",[22,31955,31956],{},"utils"," directory and its subdirectories becomes available globally in your application. For a cleaner implementation, we'll take advantage of this auto-import feature in Nitro.",[27,31959,31960,31961,31964,31965,31968],{},"For our user endpoints, we'll define utility functions inside the ",[22,31962,31963],{},"\u002Futils"," directory. Let's define the following file inside the ",[22,31966,31967],{},"utils\u002Fuser.ts"," directory:",[128,31970,31972],{"className":13299,"code":31971,"language":13301,"meta":133,"style":133},"import dataSource from \"..\u002Fconfig\u002Ftypeorm.config\";\nimport { User } from \"..\u002Fentity\u002FUser\";\n\nexport async function createUser(body: User) {\n    const user = new User();\n    user.firstName = body.firstName;\n    user.lastName = body.lastName;\n    user.age = body.age;\n\n    try {\n        await dataSource.manager.save(user);\n        return { success: true, message: \"User created\" };\n    } catch (error) {\n        console.error(error);\n        return { success: false, message: \"Failed to create user\" };\n    }\n}\n",[22,31973,31974,31986,31998,32002,32023,32037,32047,32057,32067,32071,32077,32090,32108,32116,32124,32139,32143],{"__ignoreMap":133},[137,31975,31976,31978,31980,31982,31984],{"class":139,"line":140},[137,31977,10287],{"class":143},[137,31979,31781],{"class":157},[137,31981,10954],{"class":143},[137,31983,31786],{"class":284},[137,31985,3276],{"class":157},[137,31987,31988,31990,31992,31994,31996],{"class":139,"line":173},[137,31989,10287],{"class":143},[137,31991,31622],{"class":157},[137,31993,10954],{"class":143},[137,31995,31627],{"class":284},[137,31997,3276],{"class":157},[137,31999,32000],{"class":139,"line":188},[137,32001,516],{"emptyLinePlaceholder":515},[137,32003,32004,32006,32009,32011,32013,32015,32017,32019,32021],{"class":139,"line":269},[137,32005,13456],{"class":143},[137,32007,32008],{"class":143}," async",[137,32010,154],{"class":143},[137,32012,15549],{"class":147},[137,32014,356],{"class":157},[137,32016,4065],{"class":161},[137,32018,894],{"class":143},[137,32020,24658],{"class":147},[137,32022,170],{"class":157},[137,32024,32025,32027,32029,32031,32033,32035],{"class":139,"line":278},[137,32026,4177],{"class":143},[137,32028,13217],{"class":364},[137,32030,151],{"class":143},[137,32032,1426],{"class":143},[137,32034,24658],{"class":147},[137,32036,924],{"class":157},[137,32038,32039,32042,32044],{"class":139,"line":291},[137,32040,32041],{"class":157},"    user.firstName ",[137,32043,253],{"class":143},[137,32045,32046],{"class":157}," body.firstName;\n",[137,32048,32049,32052,32054],{"class":139,"line":297},[137,32050,32051],{"class":157},"    user.lastName ",[137,32053,253],{"class":143},[137,32055,32056],{"class":157}," body.lastName;\n",[137,32058,32059,32062,32064],{"class":139,"line":302},[137,32060,32061],{"class":157},"    user.age ",[137,32063,253],{"class":143},[137,32065,32066],{"class":157}," body.age;\n",[137,32068,32069],{"class":139,"line":662},[137,32070,516],{"emptyLinePlaceholder":515},[137,32072,32073,32075],{"class":139,"line":667},[137,32074,25035],{"class":143},[137,32076,256],{"class":157},[137,32078,32079,32081,32084,32087],{"class":139,"line":786},[137,32080,25043],{"class":143},[137,32082,32083],{"class":157}," dataSource.manager.",[137,32085,32086],{"class":147},"save",[137,32088,32089],{"class":157},"(user);\n",[137,32091,32092,32094,32097,32099,32102,32105],{"class":139,"line":798},[137,32093,5472],{"class":143},[137,32095,32096],{"class":157}," { success: ",[137,32098,3097],{"class":364},[137,32100,32101],{"class":157},", message: ",[137,32103,32104],{"class":284},"\"User created\"",[137,32106,32107],{"class":157}," };\n",[137,32109,32110,32112,32114],{"class":139,"line":803},[137,32111,24944],{"class":157},[137,32113,2807],{"class":143},[137,32115,15734],{"class":157},[137,32117,32118,32120,32122],{"class":139,"line":931},[137,32119,350],{"class":157},[137,32121,2812],{"class":147},[137,32123,25115],{"class":157},[137,32125,32126,32128,32130,32132,32134,32137],{"class":139,"line":1568},[137,32127,5472],{"class":143},[137,32129,32096],{"class":157},[137,32131,30105],{"class":364},[137,32133,32101],{"class":157},[137,32135,32136],{"class":284},"\"Failed to create user\"",[137,32138,32107],{"class":157},[137,32140,32141],{"class":139,"line":1573},[137,32142,294],{"class":157},[137,32144,32145],{"class":139,"line":1578},[137,32146,510],{"class":157},[27,32148,32149],{},"Here, we're defining the logic for creating a user by utilising the ORM capability in TypeORM.",[27,32151,32152,32153,32156,32157,1017],{},"Then, inside the ",[22,32154,32155],{},"\u002Froutes"," directory, we're going to define the following file ",[22,32158,32159],{},"routes\u002Fcreate-user.post.ts",[128,32161,32163],{"className":13299,"code":32162,"language":13301,"meta":133,"style":133},"export default defineEventHandler(async (event) => {\n    const body = await readBody(event);\n    return await createUser(body);\n});\n",[22,32164,32165,32188,32205,32216],{"__ignoreMap":133},[137,32166,32167,32169,32171,32174,32176,32178,32180,32182,32184,32186],{"class":139,"line":140},[137,32168,13456],{"class":143},[137,32170,21723],{"class":143},[137,32172,32173],{"class":147}," defineEventHandler",[137,32175,356],{"class":157},[137,32177,15050],{"class":143},[137,32179,158],{"class":157},[137,32181,24689],{"class":161},[137,32183,219],{"class":157},[137,32185,222],{"class":143},[137,32187,256],{"class":157},[137,32189,32190,32192,32195,32197,32199,32202],{"class":139,"line":173},[137,32191,4177],{"class":143},[137,32193,32194],{"class":364}," body",[137,32196,151],{"class":143},[137,32198,15069],{"class":143},[137,32200,32201],{"class":147}," readBody",[137,32203,32204],{"class":157},"(event);\n",[137,32206,32207,32209,32211,32213],{"class":139,"line":188},[137,32208,176],{"class":143},[137,32210,15069],{"class":143},[137,32212,15549],{"class":147},[137,32214,32215],{"class":157},"(body);\n",[137,32217,32218],{"class":139,"line":269},[137,32219,5422],{"class":157},[27,32221,32222,32223,32225,32226,32228],{},"This will automatically create an endpoint ",[22,32224,31923],{}," that will receive a ",[22,32227,12],{}," request.",[27,32230,32231,32232,32235],{},"We use the Nitro function ",[22,32233,32234],{},"readBody"," to access the body of the request. This function is automatically imported by Nitro, so we don't need to import it separately.",[27,32237,32238],{},"You can call this endpoint with the following body payload:",[128,32240,32242],{"className":5155,"code":32241,"language":5157,"meta":133,"style":133},"POST http:\u002F\u002Flocalhost:3000\u002Fcreate-user\nContent-Type: application\u002Fjson\n{\n    \"firstName\": \"Aleks\",\n    \"lastName\": \"Trpkovski\",\n    \"age\": 43\n}\n",[22,32243,32244,32252,32256,32260,32271,32283,32293],{"__ignoreMap":133},[137,32245,32246,32249],{"class":139,"line":140},[137,32247,32248],{"class":157},"POST http:",[137,32250,32251],{"class":308},"\u002F\u002Flocalhost:3000\u002Fcreate-user\n",[137,32253,32254],{"class":139,"line":173},[137,32255,15962],{"class":157},[137,32257,32258],{"class":139,"line":188},[137,32259,15971],{"class":157},[137,32261,32262,32265,32267,32269],{"class":139,"line":269},[137,32263,32264],{"class":364},"    \"firstName\"",[137,32266,726],{"class":157},[137,32268,1958],{"class":284},[137,32270,1961],{"class":157},[137,32272,32273,32276,32278,32281],{"class":139,"line":278},[137,32274,32275],{"class":364},"    \"lastName\"",[137,32277,726],{"class":157},[137,32279,32280],{"class":284},"\"Trpkovski\"",[137,32282,1961],{"class":157},[137,32284,32285,32288,32290],{"class":139,"line":291},[137,32286,32287],{"class":364},"    \"age\"",[137,32289,726],{"class":157},[137,32291,32292],{"class":364},"43\n",[137,32294,32295],{"class":139,"line":297},[137,32296,510],{"class":157},[123,32298,32300],{"id":32299},"read-users-endpoint","Read users endpoint",[27,32302,32303,32304,32306],{},"Similar to how we define the user creation endpoint, we will now create an endpoint to retrieve all users. In the user utility function ",[22,32305,31967],{},", we will add another function to return all users:",[128,32308,32310],{"className":13299,"code":32309,"language":13301,"meta":133,"style":133},"export async function getUsers() {\n    try {\n        const users = await dataSource.manager.find(User);\n        return { users };\n    } catch (error) {\n        console.error(error);\n        return { success: false, message: \"Failed to get users\" };\n    }\n}\n",[22,32311,32312,32325,32331,32350,32357,32365,32373,32388,32392],{"__ignoreMap":133},[137,32313,32314,32316,32318,32320,32323],{"class":139,"line":140},[137,32315,13456],{"class":143},[137,32317,32008],{"class":143},[137,32319,154],{"class":143},[137,32321,32322],{"class":147}," getUsers",[137,32324,275],{"class":157},[137,32326,32327,32329],{"class":139,"line":173},[137,32328,25035],{"class":143},[137,32330,256],{"class":157},[137,32332,32333,32335,32338,32340,32342,32344,32347],{"class":139,"line":188},[137,32334,3008],{"class":143},[137,32336,32337],{"class":364}," users",[137,32339,151],{"class":143},[137,32341,15069],{"class":143},[137,32343,32083],{"class":157},[137,32345,32346],{"class":147},"find",[137,32348,32349],{"class":157},"(User);\n",[137,32351,32352,32354],{"class":139,"line":269},[137,32353,5472],{"class":143},[137,32355,32356],{"class":157}," { users };\n",[137,32358,32359,32361,32363],{"class":139,"line":278},[137,32360,24944],{"class":157},[137,32362,2807],{"class":143},[137,32364,15734],{"class":157},[137,32366,32367,32369,32371],{"class":139,"line":291},[137,32368,350],{"class":157},[137,32370,2812],{"class":147},[137,32372,25115],{"class":157},[137,32374,32375,32377,32379,32381,32383,32386],{"class":139,"line":297},[137,32376,5472],{"class":143},[137,32378,32096],{"class":157},[137,32380,30105],{"class":364},[137,32382,32101],{"class":157},[137,32384,32385],{"class":284},"\"Failed to get users\"",[137,32387,32107],{"class":157},[137,32389,32390],{"class":139,"line":302},[137,32391,294],{"class":157},[137,32393,32394],{"class":139,"line":662},[137,32395,510],{"class":157},[27,32397,32398,32399,32401,32402,1017],{},"Next, within the ",[22,32400,32155],{}," directory, we will define the following file ",[22,32403,32404],{},"routes\u002Fget-users.get.ts",[128,32406,32408],{"className":13299,"code":32407,"language":13301,"meta":133,"style":133},"export default defineEventHandler(async (event) => {\n    return await getUsers();\n});\n",[22,32409,32410,32432,32442],{"__ignoreMap":133},[137,32411,32412,32414,32416,32418,32420,32422,32424,32426,32428,32430],{"class":139,"line":140},[137,32413,13456],{"class":143},[137,32415,21723],{"class":143},[137,32417,32173],{"class":147},[137,32419,356],{"class":157},[137,32421,15050],{"class":143},[137,32423,158],{"class":157},[137,32425,24689],{"class":161},[137,32427,219],{"class":157},[137,32429,222],{"class":143},[137,32431,256],{"class":157},[137,32433,32434,32436,32438,32440],{"class":139,"line":173},[137,32435,176],{"class":143},[137,32437,15069],{"class":143},[137,32439,32322],{"class":147},[137,32441,924],{"class":157},[137,32443,32444],{"class":139,"line":188},[137,32445,5422],{"class":157},[27,32447,32448],{},"The above endpoint will return all the users from the database.",[123,32450,32452],{"id":32451},"read-a-single-user-endpoint","Read a single user endpoint",[27,32454,32455,32456,894],{},"Finally, we will create an endpoint that, given an id, returns a single user associated with that id. Similar to the previous process, we are adding another function to the utility function ",[22,32457,31967],{},[128,32459,32461],{"className":13299,"code":32460,"language":13301,"meta":133,"style":133},"export async function getUser(id: number) {\n    try {\n        const user = await dataSource.manager.findOne(User, {\n            where: { id },\n        });\n        return { user };\n    } catch (error) {\n        console.error(error);\n        return { success: false, message: \"Failed to get users\" };\n    }\n}\n",[22,32462,32463,32484,32490,32508,32513,32517,32524,32532,32540,32554,32558],{"__ignoreMap":133},[137,32464,32465,32467,32469,32471,32474,32476,32478,32480,32482],{"class":139,"line":140},[137,32466,13456],{"class":143},[137,32468,32008],{"class":143},[137,32470,154],{"class":143},[137,32472,32473],{"class":147}," getUser",[137,32475,356],{"class":157},[137,32477,31478],{"class":161},[137,32479,894],{"class":143},[137,32481,31395],{"class":364},[137,32483,170],{"class":157},[137,32485,32486,32488],{"class":139,"line":173},[137,32487,25035],{"class":143},[137,32489,256],{"class":157},[137,32491,32492,32494,32496,32498,32500,32502,32505],{"class":139,"line":188},[137,32493,3008],{"class":143},[137,32495,13217],{"class":364},[137,32497,151],{"class":143},[137,32499,15069],{"class":143},[137,32501,32083],{"class":157},[137,32503,32504],{"class":147},"findOne",[137,32506,32507],{"class":157},"(User, {\n",[137,32509,32510],{"class":139,"line":269},[137,32511,32512],{"class":157},"            where: { id },\n",[137,32514,32515],{"class":139,"line":278},[137,32516,14079],{"class":157},[137,32518,32519,32521],{"class":139,"line":291},[137,32520,5472],{"class":143},[137,32522,32523],{"class":157}," { user };\n",[137,32525,32526,32528,32530],{"class":139,"line":297},[137,32527,24944],{"class":157},[137,32529,2807],{"class":143},[137,32531,15734],{"class":157},[137,32533,32534,32536,32538],{"class":139,"line":302},[137,32535,350],{"class":157},[137,32537,2812],{"class":147},[137,32539,25115],{"class":157},[137,32541,32542,32544,32546,32548,32550,32552],{"class":139,"line":662},[137,32543,5472],{"class":143},[137,32545,32096],{"class":157},[137,32547,30105],{"class":364},[137,32549,32101],{"class":157},[137,32551,32385],{"class":284},[137,32553,32107],{"class":157},[137,32555,32556],{"class":139,"line":667},[137,32557,294],{"class":157},[137,32559,32560],{"class":139,"line":786},[137,32561,510],{"class":157},[27,32563,32564,32565,32568,32569,13801,32571,32574,32575,32578,32579,32581,32582,32584,32585,32587],{},"Next, create the endpoint ",[22,32566,32567],{},"routes\u002Fget-user\u002F[id].get.ts",". Note that we've created a new directory inside the ",[22,32570,32155],{},[22,32572,32573],{},"\u002Fget-user",", and inside this directory, a file named ",[22,32576,32577],{},"[id].get.ts",". If you're familiar with Nuxt.js, this pattern will be recognisable, as Nitro also uses it. The name within the square brackets represents the name of the dynamic parameter we want to pass to the endpoint. For example, in our case, the endpoint ",[22,32580,31939],{}," will have a dynamic ",[22,32583,31478],{}," that can be any user id. Here's how the ",[22,32586,32577],{}," file should be implemented:",[128,32589,32591],{"className":13299,"code":32590,"language":13301,"meta":133,"style":133},"export default defineEventHandler(async (event) => {\n    const userId = Number(getRouterParam(event, \"id\"));\n    return await getUser(userId);\n});\n",[22,32592,32593,32615,32640,32651],{"__ignoreMap":133},[137,32594,32595,32597,32599,32601,32603,32605,32607,32609,32611,32613],{"class":139,"line":140},[137,32596,13456],{"class":143},[137,32598,21723],{"class":143},[137,32600,32173],{"class":147},[137,32602,356],{"class":157},[137,32604,15050],{"class":143},[137,32606,158],{"class":157},[137,32608,24689],{"class":161},[137,32610,219],{"class":157},[137,32612,222],{"class":143},[137,32614,256],{"class":157},[137,32616,32617,32619,32622,32624,32627,32629,32632,32635,32638],{"class":139,"line":173},[137,32618,4177],{"class":143},[137,32620,32621],{"class":364}," userId",[137,32623,151],{"class":143},[137,32625,32626],{"class":147}," Number",[137,32628,356],{"class":157},[137,32630,32631],{"class":147},"getRouterParam",[137,32633,32634],{"class":157},"(event, ",[137,32636,32637],{"class":284},"\"id\"",[137,32639,8614],{"class":157},[137,32641,32642,32644,32646,32648],{"class":139,"line":188},[137,32643,176],{"class":143},[137,32645,15069],{"class":143},[137,32647,32473],{"class":147},[137,32649,32650],{"class":157},"(userId);\n",[137,32652,32653],{"class":139,"line":269},[137,32654,5422],{"class":157},[27,32656,32231,32657,32659],{},[22,32658,32631],{}," to access the parameter's id. This function is automatically imported by Nitro, so we don't need to import it separately.",[27,32661,32662],{},"With that, we've defined the three endpoints for creating and reading users. Next, we'll show how to validate data using DTOs, ensuring the parameters passed during user creation follows the database schema and prevent the inclusion of unsupported types.",[104,32664,32666],{"id":32665},"use-dto-to-validate-the-request","Use DTO to validate the request",[27,32668,32669],{},"DTOs are a design pattern used in applications to hold and transfer data between different layers. One common application of DTOs is to define validation logic directly within their classes or functions, offering control over validation rules. This can be beneficial for validating data on each request, ensuring data integrity before it reaches the database.",[27,32671,32672,32673,32676,32677,32679,32680,32682],{},"Currently, our ",[22,32674,32675],{},"create-user"," endpoint can accept any type of data. For instance, a number could be passed into the ",[22,32678,4693],{}," field, or a string could replace a number in the ",[22,32681,1051],{}," property. To prevent these scenarios, we need to implement validation rules.",[27,32684,32685,32686,32688],{},"To address this, we will use the ",[22,32687,14527],{}," library. This library employs classes and TypeScript decorators, similar to TypeORM, to define validation rules. Let's start by installing the library:",[128,32690,32692],{"className":8665,"code":32691,"language":8667,"meta":133,"style":133},"yarn add class-validator\n",[22,32693,32694],{"__ignoreMap":133},[137,32695,32696,32698,32700],{"class":139,"line":140},[137,32697,17263],{"class":147},[137,32699,17266],{"class":284},[137,32701,32702],{"class":284}," class-validator\n",[27,32704,32705,32706,32709,32710,32713],{},"First, we will create a directory at the root of the project called ",[22,32707,32708],{},"\u002Fdtos",". Inside this directory, we will create the following file: ",[22,32711,32712],{},"create-user.dto.ts",", where we define the validation.",[128,32715,32717],{"className":13299,"code":32716,"language":13301,"meta":133,"style":133},"import { validateOrReject, IsString, IsNotEmpty, IsAlpha, IsNumber } from \"class-validator\";\n\nexport class CreateUserDto {\n    @IsString()\n    @IsNotEmpty()\n    @IsAlpha()\n    firstName: string;\n\n    @IsString()\n    @IsNotEmpty()\n    @IsAlpha()\n    lastName: string;\n\n    @IsNumber()\n    @IsNotEmpty()\n    @Max(99)\n    age: number;\n}\n\nconst createUserDto = defineEventHandler(async (event) => {\n    const userRequestBody = await readBody(event);\n\n    const user = new CreateUserDto();\n    user.firstName = userRequestBody.firstName;\n    user.lastName = userRequestBody.lastName;\n    user.age = userRequestBody.age;\n\n    try {\n        await validateOrReject(user);\n    } catch (errors) {\n        const res = event.node.res;\n        res.statusCode = 400;\n        res.end(JSON.stringify(errors, null, 2));\n    }\n});\n\nexport default createUserDto;\n",[22,32718,32719,32732,32736,32747,32756,32764,32772,32782,32786,32794,32802,32810,32820,32824,32833,32841,32855,32865,32869,32873,32898,32913,32917,32931,32940,32949,32958,32962,32968,32977,32986,32998,33010,33037,33041,33045,33049],{"__ignoreMap":133},[137,32720,32721,32723,32726,32728,32730],{"class":139,"line":140},[137,32722,10287],{"class":143},[137,32724,32725],{"class":157}," { validateOrReject, IsString, IsNotEmpty, IsAlpha, IsNumber } ",[137,32727,10954],{"class":143},[137,32729,14588],{"class":284},[137,32731,3276],{"class":157},[137,32733,32734],{"class":139,"line":173},[137,32735,516],{"emptyLinePlaceholder":515},[137,32737,32738,32740,32742,32745],{"class":139,"line":188},[137,32739,13456],{"class":143},[137,32741,7832],{"class":143},[137,32743,32744],{"class":147}," CreateUserDto",[137,32746,256],{"class":157},[137,32748,32749,32751,32754],{"class":139,"line":269},[137,32750,14664],{"class":157},[137,32752,32753],{"class":147},"IsString",[137,32755,2754],{"class":157},[137,32757,32758,32760,32762],{"class":139,"line":278},[137,32759,14664],{"class":157},[137,32761,14676],{"class":147},[137,32763,2754],{"class":157},[137,32765,32766,32768,32770],{"class":139,"line":291},[137,32767,14664],{"class":157},[137,32769,14861],{"class":147},[137,32771,2754],{"class":157},[137,32773,32774,32776,32778,32780],{"class":139,"line":297},[137,32775,14868],{"class":161},[137,32777,894],{"class":143},[137,32779,13630],{"class":364},[137,32781,3276],{"class":157},[137,32783,32784],{"class":139,"line":302},[137,32785,516],{"emptyLinePlaceholder":515},[137,32787,32788,32790,32792],{"class":139,"line":662},[137,32789,14664],{"class":157},[137,32791,32753],{"class":147},[137,32793,2754],{"class":157},[137,32795,32796,32798,32800],{"class":139,"line":667},[137,32797,14664],{"class":157},[137,32799,14676],{"class":147},[137,32801,2754],{"class":157},[137,32803,32804,32806,32808],{"class":139,"line":786},[137,32805,14664],{"class":157},[137,32807,14861],{"class":147},[137,32809,2754],{"class":157},[137,32811,32812,32814,32816,32818],{"class":139,"line":798},[137,32813,14923],{"class":161},[137,32815,894],{"class":143},[137,32817,13630],{"class":364},[137,32819,3276],{"class":157},[137,32821,32822],{"class":139,"line":803},[137,32823,516],{"emptyLinePlaceholder":515},[137,32825,32826,32828,32831],{"class":139,"line":931},[137,32827,14664],{"class":157},[137,32829,32830],{"class":147},"IsNumber",[137,32832,2754],{"class":157},[137,32834,32835,32837,32839],{"class":139,"line":1568},[137,32836,14664],{"class":157},[137,32838,14676],{"class":147},[137,32840,2754],{"class":157},[137,32842,32843,32845,32848,32850,32853],{"class":139,"line":1573},[137,32844,14664],{"class":157},[137,32846,32847],{"class":147},"Max",[137,32849,356],{"class":157},[137,32851,32852],{"class":364},"99",[137,32854,3155],{"class":157},[137,32856,32857,32859,32861,32863],{"class":139,"line":1578},[137,32858,31459],{"class":161},[137,32860,894],{"class":143},[137,32862,31395],{"class":364},[137,32864,3276],{"class":157},[137,32866,32867],{"class":139,"line":1588},[137,32868,510],{"class":157},[137,32870,32871],{"class":139,"line":1601},[137,32872,516],{"emptyLinePlaceholder":515},[137,32874,32875,32877,32880,32882,32884,32886,32888,32890,32892,32894,32896],{"class":139,"line":3802},[137,32876,3077],{"class":143},[137,32878,32879],{"class":364}," createUserDto",[137,32881,151],{"class":143},[137,32883,32173],{"class":147},[137,32885,356],{"class":157},[137,32887,15050],{"class":143},[137,32889,158],{"class":157},[137,32891,24689],{"class":161},[137,32893,219],{"class":157},[137,32895,222],{"class":143},[137,32897,256],{"class":157},[137,32899,32900,32902,32905,32907,32909,32911],{"class":139,"line":3808},[137,32901,4177],{"class":143},[137,32903,32904],{"class":364}," userRequestBody",[137,32906,151],{"class":143},[137,32908,15069],{"class":143},[137,32910,32201],{"class":147},[137,32912,32204],{"class":157},[137,32914,32915],{"class":139,"line":3822},[137,32916,516],{"emptyLinePlaceholder":515},[137,32918,32919,32921,32923,32925,32927,32929],{"class":139,"line":3827},[137,32920,4177],{"class":143},[137,32922,13217],{"class":364},[137,32924,151],{"class":143},[137,32926,1426],{"class":143},[137,32928,32744],{"class":147},[137,32930,924],{"class":157},[137,32932,32933,32935,32937],{"class":139,"line":3832},[137,32934,32041],{"class":157},[137,32936,253],{"class":143},[137,32938,32939],{"class":157}," userRequestBody.firstName;\n",[137,32941,32942,32944,32946],{"class":139,"line":3840},[137,32943,32051],{"class":157},[137,32945,253],{"class":143},[137,32947,32948],{"class":157}," userRequestBody.lastName;\n",[137,32950,32951,32953,32955],{"class":139,"line":3846},[137,32952,32061],{"class":157},[137,32954,253],{"class":143},[137,32956,32957],{"class":157}," userRequestBody.age;\n",[137,32959,32960],{"class":139,"line":3861},[137,32961,516],{"emptyLinePlaceholder":515},[137,32963,32964,32966],{"class":139,"line":3883},[137,32965,25035],{"class":143},[137,32967,256],{"class":157},[137,32969,32970,32972,32975],{"class":139,"line":3896},[137,32971,25043],{"class":143},[137,32973,32974],{"class":147}," validateOrReject",[137,32976,32089],{"class":157},[137,32978,32979,32981,32983],{"class":139,"line":3901},[137,32980,24944],{"class":157},[137,32982,2807],{"class":143},[137,32984,32985],{"class":157}," (errors) {\n",[137,32987,32988,32990,32993,32995],{"class":139,"line":3906},[137,32989,3008],{"class":143},[137,32991,32992],{"class":364}," res",[137,32994,151],{"class":143},[137,32996,32997],{"class":157}," event.node.res;\n",[137,32999,33000,33003,33005,33008],{"class":139,"line":3911},[137,33001,33002],{"class":157},"        res.statusCode ",[137,33004,253],{"class":143},[137,33006,33007],{"class":364}," 400",[137,33009,3276],{"class":157},[137,33011,33012,33015,33018,33020,33022,33024,33026,33029,33031,33033,33035],{"class":139,"line":4666},[137,33013,33014],{"class":157},"        res.",[137,33016,33017],{"class":147},"end",[137,33019,356],{"class":157},[137,33021,22554],{"class":364},[137,33023,1017],{"class":157},[137,33025,24816],{"class":147},[137,33027,33028],{"class":157},"(errors, ",[137,33030,11700],{"class":364},[137,33032,164],{"class":157},[137,33034,10345],{"class":364},[137,33036,8614],{"class":157},[137,33038,33039],{"class":139,"line":4672},[137,33040,294],{"class":157},[137,33042,33043],{"class":139,"line":4680},[137,33044,5422],{"class":157},[137,33046,33047],{"class":139,"line":4711},[137,33048,516],{"emptyLinePlaceholder":515},[137,33050,33051,33053,33055],{"class":139,"line":4716},[137,33052,13456],{"class":143},[137,33054,21723],{"class":143},[137,33056,33057],{"class":157}," createUserDto;\n",[27,33059,33060,33061,33064,33065,33067,33068,1017],{},"Firstly, we will define the validation schema class ",[22,33062,33063],{},"CreateUserDto",". We use decorators to define validation rules for each field. For more details on available decorators, refer to the ",[22,33066,14527],{}," package documentation ",[45,33069,10647],{"href":20911,"target":2716,"rel":33070},[2718,2719],[27,33072,33073,33074,33077,33078,33081,33082,33084],{},"You might notice that this function looks very similar to the routes functions. We use the ",[22,33075,33076],{},"defineEventHandler"," handler and read the event body same as we do in the ",[22,33079,33080],{},"create-user.post.ts"," route. To give you context, this is a middleware that we will add to the existing ",[22,33083,33080],{}," function to handle validation for each request. If the body data does not follow the rule, it will return a 400 error with an explanation of the error.",[27,33086,33087,33088,33090],{},"Let's see how we can do that. Modify the ",[22,33089,33080],{}," as follows:",[128,33092,33094],{"className":13299,"code":33093,"language":13301,"meta":133,"style":133},"import createUserDto from \"..\u002Fdtos\u002Fcreate-user.dto\";\n\nexport default defineEventHandler({\n    onRequest: [createUserDto],\n    async handler(event) {\n        const body = await readBody(event);\n        return await createUser(body);\n    },\n});\n",[22,33095,33096,33110,33114,33124,33129,33142,33156,33166,33170],{"__ignoreMap":133},[137,33097,33098,33100,33103,33105,33108],{"class":139,"line":140},[137,33099,10287],{"class":143},[137,33101,33102],{"class":157}," createUserDto ",[137,33104,10954],{"class":143},[137,33106,33107],{"class":284}," \"..\u002Fdtos\u002Fcreate-user.dto\"",[137,33109,3276],{"class":157},[137,33111,33112],{"class":139,"line":173},[137,33113,516],{"emptyLinePlaceholder":515},[137,33115,33116,33118,33120,33122],{"class":139,"line":188},[137,33117,13456],{"class":143},[137,33119,21723],{"class":143},[137,33121,32173],{"class":147},[137,33123,3175],{"class":157},[137,33125,33126],{"class":139,"line":269},[137,33127,33128],{"class":157},"    onRequest: [createUserDto],\n",[137,33130,33131,33133,33136,33138,33140],{"class":139,"line":278},[137,33132,15546],{"class":143},[137,33134,33135],{"class":147}," handler",[137,33137,356],{"class":157},[137,33139,24689],{"class":161},[137,33141,170],{"class":157},[137,33143,33144,33146,33148,33150,33152,33154],{"class":139,"line":291},[137,33145,3008],{"class":143},[137,33147,32194],{"class":364},[137,33149,151],{"class":143},[137,33151,15069],{"class":143},[137,33153,32201],{"class":147},[137,33155,32204],{"class":157},[137,33157,33158,33160,33162,33164],{"class":139,"line":297},[137,33159,5472],{"class":143},[137,33161,15069],{"class":143},[137,33163,15549],{"class":147},[137,33165,32215],{"class":157},[137,33167,33168],{"class":139,"line":302},[137,33169,775],{"class":157},[137,33171,33172],{"class":139,"line":662},[137,33173,5422],{"class":157},[27,33175,33176,33177,33179,33180,33183],{},"As you can see, we added an ",[22,33178,9056],{}," property and included the ",[22,33181,33182],{},"createUserDto"," function in the array. This will be run first before each request and validate the data before continuing with any other operation. If the rules are broken, the request will not continue.",[27,33185,33186,33187,33189],{},"For example, if we added a number to the ",[22,33188,4693],{}," and the rules are set to accept only alphabetic characters, we would get the following error:",[128,33191,33193],{"className":5155,"code":33192,"language":5157,"meta":133,"style":133},"[\n    {\n        \"target\": {\n            \"firstName\": \"Nicole123\",\n            \"lastName\": \"Tesla\",\n            \"age\": 43\n        },\n        \"value\": \"Nicole123\",\n        \"property\": \"firstName\",\n        \"children\": [],\n        \"constraints\": {\n            \"isAlpha\": \"firstName must contain only letters (a-zA-Z)\"\n        }\n    }\n]\n",[22,33194,33195,33199,33203,33210,33222,33234,33243,33247,33258,33270,33278,33285,33295,33299,33303],{"__ignoreMap":133},[137,33196,33197],{"class":139,"line":140},[137,33198,28046],{"class":157},[137,33200,33201],{"class":139,"line":173},[137,33202,28051],{"class":157},[137,33204,33205,33208],{"class":139,"line":188},[137,33206,33207],{"class":364},"        \"target\"",[137,33209,1819],{"class":157},[137,33211,33212,33215,33217,33220],{"class":139,"line":269},[137,33213,33214],{"class":364},"            \"firstName\"",[137,33216,726],{"class":157},[137,33218,33219],{"class":284},"\"Nicole123\"",[137,33221,1961],{"class":157},[137,33223,33224,33227,33229,33232],{"class":139,"line":278},[137,33225,33226],{"class":364},"            \"lastName\"",[137,33228,726],{"class":157},[137,33230,33231],{"class":284},"\"Tesla\"",[137,33233,1961],{"class":157},[137,33235,33236,33239,33241],{"class":139,"line":291},[137,33237,33238],{"class":364},"            \"age\"",[137,33240,726],{"class":157},[137,33242,32292],{"class":364},[137,33244,33245],{"class":139,"line":297},[137,33246,2084],{"class":157},[137,33248,33249,33252,33254,33256],{"class":139,"line":302},[137,33250,33251],{"class":364},"        \"value\"",[137,33253,726],{"class":157},[137,33255,33219],{"class":284},[137,33257,1961],{"class":157},[137,33259,33260,33263,33265,33268],{"class":139,"line":662},[137,33261,33262],{"class":364},"        \"property\"",[137,33264,726],{"class":157},[137,33266,33267],{"class":284},"\"firstName\"",[137,33269,1961],{"class":157},[137,33271,33272,33275],{"class":139,"line":667},[137,33273,33274],{"class":364},"        \"children\"",[137,33276,33277],{"class":157},": [],\n",[137,33279,33280,33283],{"class":139,"line":786},[137,33281,33282],{"class":364},"        \"constraints\"",[137,33284,1819],{"class":157},[137,33286,33287,33290,33292],{"class":139,"line":798},[137,33288,33289],{"class":364},"            \"isAlpha\"",[137,33291,726],{"class":157},[137,33293,33294],{"class":284},"\"firstName must contain only letters (a-zA-Z)\"\n",[137,33296,33297],{"class":139,"line":803},[137,33298,1966],{"class":157},[137,33300,33301],{"class":139,"line":931},[137,33302,294],{"class":157},[137,33304,33305],{"class":139,"line":1568},[137,33306,33307],{"class":157},"]\n",[27,33309,33310],{},"Or if we add an age of 101, this will result in an error because we have set rules to accept a maximum age of 99.",[128,33312,33314],{"className":5155,"code":33313,"language":5157,"meta":133,"style":133},"{\n    \"target\": {\n      \"firstName\": \"Nicole\",\n      \"lastName\": \"Tesla\",\n      \"age\": 101\n    },\n    \"value\": 101,\n    \"property\": \"age\",\n    \"children\": [],\n    \"constraints\": {\n      \"max\": \"age must not be greater than 99\"\n    }\n  }\n]\n",[22,33315,33316,33320,33327,33339,33350,33360,33364,33376,33388,33395,33402,33412,33416,33420],{"__ignoreMap":133},[137,33317,33318],{"class":139,"line":140},[137,33319,15971],{"class":157},[137,33321,33322,33325],{"class":139,"line":173},[137,33323,33324],{"class":364},"    \"target\"",[137,33326,1819],{"class":157},[137,33328,33329,33332,33334,33337],{"class":139,"line":188},[137,33330,33331],{"class":364},"      \"firstName\"",[137,33333,726],{"class":157},[137,33335,33336],{"class":284},"\"Nicole\"",[137,33338,1961],{"class":157},[137,33340,33341,33344,33346,33348],{"class":139,"line":269},[137,33342,33343],{"class":364},"      \"lastName\"",[137,33345,726],{"class":157},[137,33347,33231],{"class":284},[137,33349,1961],{"class":157},[137,33351,33352,33355,33357],{"class":139,"line":278},[137,33353,33354],{"class":364},"      \"age\"",[137,33356,726],{"class":157},[137,33358,33359],{"class":364},"101\n",[137,33361,33362],{"class":139,"line":291},[137,33363,775],{"class":157},[137,33365,33366,33369,33371,33374],{"class":139,"line":297},[137,33367,33368],{"class":364},"    \"value\"",[137,33370,726],{"class":157},[137,33372,33373],{"class":364},"101",[137,33375,1961],{"class":157},[137,33377,33378,33381,33383,33386],{"class":139,"line":302},[137,33379,33380],{"class":364},"    \"property\"",[137,33382,726],{"class":157},[137,33384,33385],{"class":284},"\"age\"",[137,33387,1961],{"class":157},[137,33389,33390,33393],{"class":139,"line":662},[137,33391,33392],{"class":364},"    \"children\"",[137,33394,33277],{"class":157},[137,33396,33397,33400],{"class":139,"line":667},[137,33398,33399],{"class":364},"    \"constraints\"",[137,33401,1819],{"class":157},[137,33403,33404,33407,33409],{"class":139,"line":786},[137,33405,33406],{"class":364},"      \"max\"",[137,33408,726],{"class":157},[137,33410,33411],{"class":284},"\"age must not be greater than 99\"\n",[137,33413,33414],{"class":139,"line":798},[137,33415,294],{"class":157},[137,33417,33418],{"class":139,"line":803},[137,33419,3462],{"class":157},[137,33421,33422],{"class":139,"line":931},[137,33423,33307],{"class":157},[104,33425,33427],{"id":33426},"cacheing","Cacheing",[27,33429,33430,33431,1017],{},"Nitro provides a caching system where we can easily cache certain routes. For more details on how caching works and what options are available, refer to the Nitro documentation ",[45,33432,10647],{"href":33433,"target":2716,"rel":33434},"https:\u002F\u002Fnitro.unjs.io\u002Fguide\u002Fcache",[2718,2719],[27,33436,33437,33438,33441],{},"For now, we will demonstrate how to modify the ",[22,33439,33440],{},"get-users.get.ts"," type to handle caching for 10 seconds. We can modify the endpoint as follows:",[128,33443,33445],{"className":13299,"code":33444,"language":13301,"meta":133,"style":133},"export default defineCachedEventHandler(\n    async () => {\n        return await getUsers();\n    },\n    { maxAge: 10 \u002F* 10 seconds *\u002F }\n);\n",[22,33446,33447,33458,33468,33478,33482,33495],{"__ignoreMap":133},[137,33448,33449,33451,33453,33456],{"class":139,"line":140},[137,33450,13456],{"class":143},[137,33452,21723],{"class":143},[137,33454,33455],{"class":147}," defineCachedEventHandler",[137,33457,11813],{"class":157},[137,33459,33460,33462,33464,33466],{"class":139,"line":173},[137,33461,15546],{"class":143},[137,33463,1484],{"class":157},[137,33465,222],{"class":143},[137,33467,256],{"class":157},[137,33469,33470,33472,33474,33476],{"class":139,"line":188},[137,33471,5472],{"class":143},[137,33473,15069],{"class":143},[137,33475,32322],{"class":147},[137,33477,924],{"class":157},[137,33479,33480],{"class":139,"line":269},[137,33481,775],{"class":157},[137,33483,33484,33487,33490,33493],{"class":139,"line":278},[137,33485,33486],{"class":157},"    { maxAge: ",[137,33488,33489],{"class":364},"10",[137,33491,33492],{"class":308}," \u002F* 10 seconds *\u002F",[137,33494,1115],{"class":157},[137,33496,33497],{"class":139,"line":291},[137,33498,1502],{"class":157},[27,33500,33501,33502,33505,33506,33509],{},"That's it. Replacing the function to ",[22,33503,33504],{},"defineCachedEventHandler"," and adding an option of ",[22,33507,33508],{},"maxAge"," will handle this automatically.",[104,33511,2567],{"id":2566},[27,33513,33514],{},"Nitro is an open-source, user-friendly backend framework for Node.js. It's maintained by the same core team behind Nuxt.js. The framework offers numerous built-in features, making it a great choice for modern web development. These features include file-based routing, auto imports, built-in middleware, caching, and a plugin-based system for easy extendability.",[27,33516,33517],{},"Aside from the built-in features in Nitro, we also demonstrate how to start and create a database using SQLite and TypeORM. This includes adding migrations and integrating well-known patterns such as DTOs for validating request.",[27,33519,33520,33521,1017],{},"The blog post above provides a basic app to help you get started. It can serve as your starting point. Feel free to check out the GitHub repository containing the code for this project ",[45,33522,10647],{"href":33523,"target":2716,"rel":33524},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fnitro-sqlite",[2718,2719],[2617,33526,33527],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":133,"searchDepth":173,"depth":173,"links":33529},[33530,33531,33537,33542,33543,33544],{"id":30788,"depth":173,"text":30789},{"id":30880,"depth":173,"text":30881,"children":33532},[33533,33534,33535,33536],{"id":31296,"depth":188,"text":31297},{"id":31488,"depth":188,"text":31489},{"id":31575,"depth":188,"text":31576},{"id":31732,"depth":188,"text":31733},{"id":31900,"depth":173,"text":31901,"children":33538},[33539,33540,33541],{"id":31945,"depth":188,"text":31946},{"id":32299,"depth":188,"text":32300},{"id":32451,"depth":188,"text":32452},{"id":32665,"depth":173,"text":32666},{"id":33426,"depth":173,"text":33427},{"id":2566,"depth":173,"text":2567},"If you're familiar with Nuxt.js, you might know that the latest version, Nuxt version 3, runs on a new server engine called Nitro. Nitro isn't just used in Nuxt, it's also an independent open-source framework for developing web server applications. It provides several built-in features that make it a modern, user-friendly backend framework. Nitro is open-source and maintained by the same core team as Nuxt.js. In this blog post, we'll show you how to build a simple web server using Nitro and connect it to an SQLite database. We'll also explain the process of database migration with TypeORM, a popular typescript library, introduce the widely-used pattern for validating request data, the Data Transfer Object (DTO), and utilise some of Nitro's built-in features to cache results.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1708844513\u002Fblog\u002Fcreate-a-web-server-using-nitro-sqlite-and-typeorm\u002Fcreate-a-web-server-using-nitro-sqlite-and-typeorm_ozdamt",[21096,33548,12817,33549,30889,33550,33551,33552,33553,5300,5299],"Nitro.js","SQLite","Data Transfer Object","DTO","Class Validator","Decorators",{},"\u002F2024\u002F02\u002F25\u002Fcreate-a-web-server-using-nitro-sqlite-and-typeorm","25th February 2024",{"title":30743,"description":33545},"2024\u002F02\u002F25\u002Fcreate-a-web-server-using-nitro-sqlite-and-typeorm","Xlyh_9uPzsOIGIK-XOqHNnbkrfpXSZX3E9kj7NaFNEc",{"id":33561,"title":33562,"articleTags":33563,"author":11,"blog":12,"body":33564,"description":34959,"extension":2649,"image":34960,"keywords":34961,"meta":34966,"navigation":515,"path":34967,"published":34968,"readTime":291,"seo":34969,"stem":34970,"type":2662,"__hash__":34971},"content\u002F2024\u002F03\u002F24\u002Fis-it-possible-to-use-nuxt-link-in-content-rendered-with-v-html.md","Is it possible to use \"\u003Cnuxt-link>\" in content rendered with \"v-html\"?",[21096,8,12817],{"type":14,"value":33565,"toc":34953},[33566,33577,33591,33593,33597,33602,33608,33621,33629,33632,33636,33646,33792,33795,33823,33828,33853,33869,34488,34491,34568,34575,34583,34791,34794,34927,34929,34950],[17,33567,33569,33570,33573,33574,12972],{"id":33568},"is-it-possible-to-use-nuxt-link-in-content-rendered-with-v-html","Is it possible to use ",[22,33571,33572],{},"\u003Cnuxt-link>"," in content rendered with ",[22,33575,33576],{},"v-html",[27,33578,33579],{},[30,33580,33581,36,33583,40,33585],{},[33,33582],{"value":35},[33,33584],{"value":39},[42,33586,33587],{},[45,33588,33589],{"href":47},[33,33590],{"value":50},[52,33592],{":tags":54},[56,33594],{":audio-src":33595,":transcript-src":33596},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2024\u002F03\u002F24\u002Fis-it-possible-to-use-nuxt-link-in-content-rendered-with-v-html\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2024\u002F03\u002F24\u002Fis-it-possible-to-use-nuxt-link-in-content-rendered-with-v-html\u002Fsummary.json",[27,33598,33599],{},[63,33600],{"alt":12847,"src":33601},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1711259337\u002Fblog\u002FIs%20it%20possible%20to%20use%20nuxt-link%20in%20content%20rendered%20with%20v-html\u002Faktookoffk14wzluq2er",[27,33603,33604,33605,33607],{},"If you've used Vue or Nuxt, you're likely familiar with the ",[22,33606,33576],{}," directive. This is used to dynamically insert HTML content into an element. It takes a string value containing HTML tags and text, replacing the inner HTML of the target element with this content.",[27,33609,33610,33611,33613,33614,33616,33617,33620],{},"One limitation of ",[22,33612,33576],{}," is that it does not allow you to render ",[22,33615,33572],{},". Instead, all ",[22,33618,33619],{},"\u003Ca>"," HTML tags do not use the native Vue router.",[27,33622,33623,33624,33626,33627,1017],{},"However, we can address this limitation by writing our own directive. This will allow all ",[22,33625,33619],{}," tags to be converted into ",[22,33628,33572],{},[27,33630,33631],{},"Thanks to Nuxt and its plugin system, we can easily expand the functionality of Nuxt. Let's get started.",[104,33633,33635],{"id":33634},"create-a-nuxt-plugin","Create a Nuxt plugin",[27,33637,33638,33639,33641,33642,33645],{},"Within the root of your Nuxt application, create a ",[22,33640,31764],{}," directory. In this directory, create a file named ",[22,33643,33644],{},"nuxt-html.ts",". The following logic will be added to this file:",[128,33647,33649],{"className":13299,"code":33648,"language":13301,"meta":133,"style":133},"export default defineNuxtPlugin((nuxtApp) => {\n    nuxtApp.vueApp.directive(\"nuxtHtml\", {\n        mounted(el: HTMLElement, binding) {\n            el.innerHTML = assignAnchorsIds(binding.value);\n            convertAnchorToNuxtLink(el);\n        },\n        updated(el, binding) {\n            el.innerHTML = assignAnchorsIds(binding.value);\n            convertAnchorToNuxtLink(el);\n        },\n        unmounted() {\n            removeListeners();\n        },\n    });\n});\n",[22,33650,33651,33671,33686,33706,33719,33727,33731,33746,33756,33762,33766,33773,33780,33784,33788],{"__ignoreMap":133},[137,33652,33653,33655,33657,33660,33662,33665,33667,33669],{"class":139,"line":140},[137,33654,13456],{"class":143},[137,33656,21723],{"class":143},[137,33658,33659],{"class":147}," defineNuxtPlugin",[137,33661,2774],{"class":157},[137,33663,33664],{"class":161},"nuxtApp",[137,33666,219],{"class":157},[137,33668,222],{"class":143},[137,33670,256],{"class":157},[137,33672,33673,33676,33679,33681,33684],{"class":139,"line":173},[137,33674,33675],{"class":157},"    nuxtApp.vueApp.",[137,33677,33678],{"class":147},"directive",[137,33680,356],{"class":157},[137,33682,33683],{"class":284},"\"nuxtHtml\"",[137,33685,5396],{"class":157},[137,33687,33688,33691,33693,33696,33698,33700,33702,33704],{"class":139,"line":188},[137,33689,33690],{"class":147},"        mounted",[137,33692,356],{"class":157},[137,33694,33695],{"class":161},"el",[137,33697,894],{"class":143},[137,33699,3644],{"class":147},[137,33701,164],{"class":157},[137,33703,33],{"class":161},[137,33705,170],{"class":157},[137,33707,33708,33711,33713,33716],{"class":139,"line":269},[137,33709,33710],{"class":157},"            el.innerHTML ",[137,33712,253],{"class":143},[137,33714,33715],{"class":147}," assignAnchorsIds",[137,33717,33718],{"class":157},"(binding.value);\n",[137,33720,33721,33724],{"class":139,"line":278},[137,33722,33723],{"class":147},"            convertAnchorToNuxtLink",[137,33725,33726],{"class":157},"(el);\n",[137,33728,33729],{"class":139,"line":291},[137,33730,2084],{"class":157},[137,33732,33733,33736,33738,33740,33742,33744],{"class":139,"line":297},[137,33734,33735],{"class":147},"        updated",[137,33737,356],{"class":157},[137,33739,33695],{"class":161},[137,33741,164],{"class":157},[137,33743,33],{"class":161},[137,33745,170],{"class":157},[137,33747,33748,33750,33752,33754],{"class":139,"line":302},[137,33749,33710],{"class":157},[137,33751,253],{"class":143},[137,33753,33715],{"class":147},[137,33755,33718],{"class":157},[137,33757,33758,33760],{"class":139,"line":662},[137,33759,33723],{"class":147},[137,33761,33726],{"class":157},[137,33763,33764],{"class":139,"line":667},[137,33765,2084],{"class":157},[137,33767,33768,33771],{"class":139,"line":786},[137,33769,33770],{"class":147},"        unmounted",[137,33772,275],{"class":157},[137,33774,33775,33778],{"class":139,"line":798},[137,33776,33777],{"class":147},"            removeListeners",[137,33779,924],{"class":157},[137,33781,33782],{"class":139,"line":803},[137,33783,2084],{"class":157},[137,33785,33786],{"class":139,"line":931},[137,33787,2832],{"class":157},[137,33789,33790],{"class":139,"line":1568},[137,33791,5422],{"class":157},[27,33793,33794],{},"Let's break down the code above to understand each part:",[1003,33796,33797,33805],{},[1006,33798,33799,33804],{},[42,33800,33801,894],{},[22,33802,33803],{},"defineNuxtPlugin"," This function creates a Nuxt plugin, making its features available throughout the application.",[1006,33806,33807,33812,33813,33816,33817,33820,33821,10277],{},[42,33808,33809,894],{},[22,33810,33811],{},"nuxtApp.vueApp.directive"," This registers a custom Vue directive called ",[22,33814,33815],{},"nuxtHtml",". We can use ",[22,33818,33819],{},"v-nuxt-html"," later in the ",[22,33822,7575],{},[27,33824,33825],{},[42,33826,33827],{},"Directive Lifecycle Hooks:",[1003,33829,33830,33837,33845],{},[1006,33831,33832,33836],{},[42,33833,33834,894],{},[22,33835,7723],{}," Executes when the element using the directive is inserted into the DOM.",[1006,33838,33839,33844],{},[42,33840,33841,894],{},[22,33842,33843],{},"updated"," Executes when the value bound to the directive changes.",[1006,33846,33847,33852],{},[42,33848,33849,894],{},[22,33850,33851],{},"unmounted"," Executes when the element is removed from the DOM.",[27,33854,33855,33856,164,33859,14528,33862,33865,33866,33868],{},"Next, let's examine the key functions we'll use within our directive: ",[22,33857,33858],{},"assignAnchorsIds",[22,33860,33861],{},"convertAnchorToNuxtLink",[22,33863,33864],{},"removeListeners",". We'll add these functions in the ",[22,33867,33644],{},", above the plugin definition.",[128,33870,33872],{"className":13299,"code":33871,"language":13301,"meta":133,"style":133},"const idsWithListeners = new Set\u003Cstring>();\n\nfunction generateUuid() {\n    return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(\u002F[xy]\u002Fg, function (c) {\n        const r = (Math.random() * 16) | 0,\n            v = c === \"x\" ? r : (r & 0x3) | 0x8;\n        return v.toString(16);\n    });\n}\n\nfunction assignAnchorsIds(html: string) {\n    const domParser = new DOMParser();\n    const doc = domParser.parseFromString(html, \"text\u002Fhtml\");\n    const anchors = doc.querySelectorAll(\"a\");\n\n    anchors.forEach((anchor) => {\n        if (!anchor.hasAttribute(\"id\")) {\n            anchor.setAttribute(\"id\", generateUuid());\n        }\n    });\n    return doc.documentElement.outerHTML;\n}\n\nfunction convertAnchorToNuxtLink(html: HTMLElement) {\n    const anchors = html.querySelectorAll(\"a\");\n\n    anchors.forEach((anchor) => {\n        if (anchor.id && !idsWithListeners.has(anchor.id)) {\n            const link = document.getElementById(anchor.id);\n\n            link?.addEventListener(\"click\", (event) => {\n                event.preventDefault();\n                const to = anchor.getAttribute(\"href\");\n                if (to) {\n                    useNuxtApp().$router.push(to);\n                }\n            });\n\n            \u002F\u002F Add the ID to the Set\n            idsWithListeners.add(anchor.id);\n        }\n    });\n}\n\nfunction removeListeners() {\n    idsWithListeners.forEach((id) => {\n        const link = document.getElementById(id);\n        link?.removeEventListener(\"click\", () => {});\n        idsWithListeners.delete(id);\n    });\n}\n",[22,33873,33874,33895,33899,33908,33943,33972,34012,34027,34031,34035,34039,34055,34071,34094,34115,34119,34137,34158,34177,34181,34185,34192,34196,34200,34217,34236,34240,34256,34276,34292,34296,34317,34326,34347,34355,34368,34373,34377,34381,34386,34396,34400,34404,34408,34412,34421,34438,34453,34470,34480,34484],{"__ignoreMap":133},[137,33875,33876,33878,33881,33883,33885,33888,33890,33892],{"class":139,"line":140},[137,33877,3077],{"class":143},[137,33879,33880],{"class":364}," idsWithListeners",[137,33882,151],{"class":143},[137,33884,1426],{"class":143},[137,33886,33887],{"class":147}," Set",[137,33889,4033],{"class":157},[137,33891,14158],{"class":364},[137,33893,33894],{"class":157},">();\n",[137,33896,33897],{"class":139,"line":173},[137,33898,516],{"emptyLinePlaceholder":515},[137,33900,33901,33903,33906],{"class":139,"line":188},[137,33902,483],{"class":143},[137,33904,33905],{"class":147}," generateUuid",[137,33907,275],{"class":157},[137,33909,33910,33912,33915,33917,33920,33922,33924,33927,33929,33932,33934,33936,33938,33941],{"class":139,"line":269},[137,33911,176],{"class":143},[137,33913,33914],{"class":284}," \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\"",[137,33916,1017],{"class":157},[137,33918,33919],{"class":147},"replace",[137,33921,356],{"class":157},[137,33923,47],{"class":284},[137,33925,33926],{"class":364},"[xy]",[137,33928,47],{"class":284},[137,33930,33931],{"class":143},"g",[137,33933,164],{"class":157},[137,33935,483],{"class":143},[137,33937,158],{"class":157},[137,33939,33940],{"class":161},"c",[137,33942,170],{"class":157},[137,33944,33945,33947,33950,33952,33955,33957,33959,33961,33964,33966,33968,33970],{"class":139,"line":278},[137,33946,3008],{"class":143},[137,33948,33949],{"class":364}," r",[137,33951,151],{"class":143},[137,33953,33954],{"class":157}," (Math.",[137,33956,7678],{"class":147},[137,33958,3348],{"class":157},[137,33960,7672],{"class":143},[137,33962,33963],{"class":364}," 16",[137,33965,219],{"class":157},[137,33967,7684],{"class":143},[137,33969,7687],{"class":364},[137,33971,1961],{"class":157},[137,33973,33974,33977,33979,33982,33984,33987,33989,33992,33994,33997,34000,34003,34005,34007,34010],{"class":139,"line":291},[137,33975,33976],{"class":364},"            v",[137,33978,151],{"class":143},[137,33980,33981],{"class":157}," c ",[137,33983,5502],{"class":143},[137,33985,33986],{"class":284}," \"x\"",[137,33988,26196],{"class":143},[137,33990,33991],{"class":157}," r ",[137,33993,894],{"class":143},[137,33995,33996],{"class":157}," (r ",[137,33998,33999],{"class":143},"&",[137,34001,34002],{"class":364}," 0x3",[137,34004,219],{"class":157},[137,34006,7684],{"class":143},[137,34008,34009],{"class":364}," 0x8",[137,34011,3276],{"class":157},[137,34013,34014,34016,34019,34021,34023,34025],{"class":139,"line":297},[137,34015,5472],{"class":143},[137,34017,34018],{"class":157}," v.",[137,34020,7692],{"class":147},[137,34022,356],{"class":157},[137,34024,7697],{"class":364},[137,34026,1502],{"class":157},[137,34028,34029],{"class":139,"line":302},[137,34030,2832],{"class":157},[137,34032,34033],{"class":139,"line":662},[137,34034,510],{"class":157},[137,34036,34037],{"class":139,"line":667},[137,34038,516],{"emptyLinePlaceholder":515},[137,34040,34041,34043,34045,34047,34049,34051,34053],{"class":139,"line":786},[137,34042,483],{"class":143},[137,34044,33715],{"class":147},[137,34046,356],{"class":157},[137,34048,4026],{"class":161},[137,34050,894],{"class":143},[137,34052,13630],{"class":364},[137,34054,170],{"class":157},[137,34056,34057,34059,34062,34064,34066,34069],{"class":139,"line":798},[137,34058,4177],{"class":143},[137,34060,34061],{"class":364}," domParser",[137,34063,151],{"class":143},[137,34065,1426],{"class":143},[137,34067,34068],{"class":147}," DOMParser",[137,34070,924],{"class":157},[137,34072,34073,34075,34078,34080,34083,34086,34089,34092],{"class":139,"line":803},[137,34074,4177],{"class":143},[137,34076,34077],{"class":364}," doc",[137,34079,151],{"class":143},[137,34081,34082],{"class":157}," domParser.",[137,34084,34085],{"class":147},"parseFromString",[137,34087,34088],{"class":157},"(html, ",[137,34090,34091],{"class":284},"\"text\u002Fhtml\"",[137,34093,1502],{"class":157},[137,34095,34096,34098,34101,34103,34106,34108,34110,34113],{"class":139,"line":931},[137,34097,4177],{"class":143},[137,34099,34100],{"class":364}," anchors",[137,34102,151],{"class":143},[137,34104,34105],{"class":157}," doc.",[137,34107,27075],{"class":147},[137,34109,356],{"class":157},[137,34111,34112],{"class":284},"\"a\"",[137,34114,1502],{"class":157},[137,34116,34117],{"class":139,"line":1568},[137,34118,516],{"emptyLinePlaceholder":515},[137,34120,34121,34124,34126,34128,34131,34133,34135],{"class":139,"line":1573},[137,34122,34123],{"class":157},"    anchors.",[137,34125,8564],{"class":147},[137,34127,2774],{"class":157},[137,34129,34130],{"class":161},"anchor",[137,34132,219],{"class":157},[137,34134,222],{"class":143},[137,34136,256],{"class":157},[137,34138,34139,34141,34143,34145,34148,34151,34153,34155],{"class":139,"line":1578},[137,34140,5496],{"class":143},[137,34142,158],{"class":157},[137,34144,17393],{"class":143},[137,34146,34147],{"class":157},"anchor.",[137,34149,34150],{"class":147},"hasAttribute",[137,34152,356],{"class":157},[137,34154,32637],{"class":284},[137,34156,34157],{"class":157},")) {\n",[137,34159,34160,34163,34166,34168,34170,34172,34175],{"class":139,"line":1588},[137,34161,34162],{"class":157},"            anchor.",[137,34164,34165],{"class":147},"setAttribute",[137,34167,356],{"class":157},[137,34169,32637],{"class":284},[137,34171,164],{"class":157},[137,34173,34174],{"class":147},"generateUuid",[137,34176,14173],{"class":157},[137,34178,34179],{"class":139,"line":1601},[137,34180,1966],{"class":157},[137,34182,34183],{"class":139,"line":3802},[137,34184,2832],{"class":157},[137,34186,34187,34189],{"class":139,"line":3808},[137,34188,176],{"class":143},[137,34190,34191],{"class":157}," doc.documentElement.outerHTML;\n",[137,34193,34194],{"class":139,"line":3822},[137,34195,510],{"class":157},[137,34197,34198],{"class":139,"line":3827},[137,34199,516],{"emptyLinePlaceholder":515},[137,34201,34202,34204,34207,34209,34211,34213,34215],{"class":139,"line":3832},[137,34203,483],{"class":143},[137,34205,34206],{"class":147}," convertAnchorToNuxtLink",[137,34208,356],{"class":157},[137,34210,4026],{"class":161},[137,34212,894],{"class":143},[137,34214,3644],{"class":147},[137,34216,170],{"class":157},[137,34218,34219,34221,34223,34225,34228,34230,34232,34234],{"class":139,"line":3840},[137,34220,4177],{"class":143},[137,34222,34100],{"class":364},[137,34224,151],{"class":143},[137,34226,34227],{"class":157}," html.",[137,34229,27075],{"class":147},[137,34231,356],{"class":157},[137,34233,34112],{"class":284},[137,34235,1502],{"class":157},[137,34237,34238],{"class":139,"line":3846},[137,34239,516],{"emptyLinePlaceholder":515},[137,34241,34242,34244,34246,34248,34250,34252,34254],{"class":139,"line":3861},[137,34243,34123],{"class":157},[137,34245,8564],{"class":147},[137,34247,2774],{"class":157},[137,34249,34130],{"class":161},[137,34251,219],{"class":157},[137,34253,222],{"class":143},[137,34255,256],{"class":157},[137,34257,34258,34260,34263,34265,34267,34270,34273],{"class":139,"line":3883},[137,34259,5496],{"class":143},[137,34261,34262],{"class":157}," (anchor.id ",[137,34264,3351],{"class":143},[137,34266,27133],{"class":143},[137,34268,34269],{"class":157},"idsWithListeners.",[137,34271,34272],{"class":147},"has",[137,34274,34275],{"class":157},"(anchor.id)) {\n",[137,34277,34278,34280,34283,34285,34287,34289],{"class":139,"line":3896},[137,34279,5772],{"class":143},[137,34281,34282],{"class":364}," link",[137,34284,151],{"class":143},[137,34286,3717],{"class":157},[137,34288,24422],{"class":147},[137,34290,34291],{"class":157},"(anchor.id);\n",[137,34293,34294],{"class":139,"line":3901},[137,34295,516],{"emptyLinePlaceholder":515},[137,34297,34298,34301,34303,34305,34307,34309,34311,34313,34315],{"class":139,"line":3906},[137,34299,34300],{"class":157},"            link?.",[137,34302,4412],{"class":147},[137,34304,356],{"class":157},[137,34306,26258],{"class":284},[137,34308,24531],{"class":157},[137,34310,24689],{"class":161},[137,34312,219],{"class":157},[137,34314,222],{"class":143},[137,34316,256],{"class":157},[137,34318,34319,34322,34324],{"class":139,"line":3911},[137,34320,34321],{"class":157},"                event.",[137,34323,24548],{"class":147},[137,34325,924],{"class":157},[137,34327,34328,34331,34333,34335,34338,34341,34343,34345],{"class":139,"line":4666},[137,34329,34330],{"class":143},"                const",[137,34332,22034],{"class":364},[137,34334,151],{"class":143},[137,34336,34337],{"class":157}," anchor.",[137,34339,34340],{"class":147},"getAttribute",[137,34342,356],{"class":157},[137,34344,29514],{"class":284},[137,34346,1502],{"class":157},[137,34348,34349,34352],{"class":139,"line":4672},[137,34350,34351],{"class":143},"                if",[137,34353,34354],{"class":157}," (to) {\n",[137,34356,34357,34360,34363,34365],{"class":139,"line":4680},[137,34358,34359],{"class":147},"                    useNuxtApp",[137,34361,34362],{"class":157},"().$router.",[137,34364,8583],{"class":147},[137,34366,34367],{"class":157},"(to);\n",[137,34369,34370],{"class":139,"line":4711},[137,34371,34372],{"class":157},"                }\n",[137,34374,34375],{"class":139,"line":4716},[137,34376,14336],{"class":157},[137,34378,34379],{"class":139,"line":4721},[137,34380,516],{"emptyLinePlaceholder":515},[137,34382,34383],{"class":139,"line":4727},[137,34384,34385],{"class":308},"            \u002F\u002F Add the ID to the Set\n",[137,34387,34388,34391,34394],{"class":139,"line":4732},[137,34389,34390],{"class":157},"            idsWithListeners.",[137,34392,34393],{"class":147},"add",[137,34395,34291],{"class":157},[137,34397,34398],{"class":139,"line":5006},[137,34399,1966],{"class":157},[137,34401,34402],{"class":139,"line":5014},[137,34403,2832],{"class":157},[137,34405,34406],{"class":139,"line":14343},[137,34407,510],{"class":157},[137,34409,34410],{"class":139,"line":24199},[137,34411,516],{"emptyLinePlaceholder":515},[137,34413,34414,34416,34419],{"class":139,"line":24773},[137,34415,483],{"class":143},[137,34417,34418],{"class":147}," removeListeners",[137,34420,275],{"class":157},[137,34422,34423,34426,34428,34430,34432,34434,34436],{"class":139,"line":24778},[137,34424,34425],{"class":157},"    idsWithListeners.",[137,34427,8564],{"class":147},[137,34429,2774],{"class":157},[137,34431,31478],{"class":161},[137,34433,219],{"class":157},[137,34435,222],{"class":143},[137,34437,256],{"class":157},[137,34439,34440,34442,34444,34446,34448,34450],{"class":139,"line":24783},[137,34441,3008],{"class":143},[137,34443,34282],{"class":364},[137,34445,151],{"class":143},[137,34447,3717],{"class":157},[137,34449,24422],{"class":147},[137,34451,34452],{"class":157},"(id);\n",[137,34454,34455,34458,34460,34462,34464,34466,34468],{"class":139,"line":24793},[137,34456,34457],{"class":157},"        link?.",[137,34459,24627],{"class":147},[137,34461,356],{"class":157},[137,34463,26258],{"class":284},[137,34465,4420],{"class":157},[137,34467,222],{"class":143},[137,34469,24638],{"class":157},[137,34471,34472,34475,34478],{"class":139,"line":24827},[137,34473,34474],{"class":157},"        idsWithListeners.",[137,34476,34477],{"class":147},"delete",[137,34479,34452],{"class":157},[137,34481,34482],{"class":139,"line":24857},[137,34483,2832],{"class":157},[137,34485,34486],{"class":139,"line":24862},[137,34487,510],{"class":157},[27,34489,34490],{},"Now, let's delve into the specifics of each function and understand how they contribute to the overall functionality of the directive.",[1003,34492,34493,34519,34554],{},[1006,34494,34495,34499],{},[42,34496,34497,894],{},[22,34498,33858],{},[1003,34500,34501,34504,34510,34516],{},[1006,34502,34503],{},"Parses the provided HTML string into a DOM document.",[1006,34505,34506,34507,34509],{},"Identifies anchor elements (",[22,34508,33619],{},") without IDs.",[1006,34511,34512,34513,34515],{},"Assigns unique IDs (using ",[22,34514,34174],{},") to those anchors for tracking and linking.",[1006,34517,34518],{},"Returns the modified HTML string.",[1006,34520,34521,34525],{},[42,34522,34523,894],{},[22,34524,33861],{},[1003,34526,34527,34530,34533],{},[1006,34528,34529],{},"Finds anchor elements within a given HTML element.",[1006,34531,34532],{},"Identifies anchors with IDs that haven't been handled yet.",[1006,34534,34535,34536],{},"Attaches click event listeners to those anchors:\n",[1003,34537,34538,34541,34547],{},[1006,34539,34540],{},"Prevents default browser behaviour (page reload).",[1006,34542,34543,34544,34546],{},"Retrieves the anchor's ",[22,34545,29284],{}," attribute.",[1006,34548,34549,34550,34553],{},"Utilises Nuxt's ",[22,34551,34552],{},"$router"," to programmatically navigate to the specified route, effectively converting basic anchors into Nuxt-powered navigation links.",[1006,34555,34556,34560],{},[42,34557,34558,894],{},[22,34559,33864],{},[1003,34561,34562,34565],{},[1006,34563,34564],{},"Iterates through the IDs of anchors with event listeners.",[1006,34566,34567],{},"Removes the click event listeners to prevent memory leaks or unintended behaviour.",[104,34569,34571,34572,34574],{"id":34570},"using-the-v-nuxt-html-directive","Using the ",[22,34573,33819],{}," directive",[27,34576,34577,34578,34580,34581,9772],{},"Now, we can use our ",[22,34579,33819],{}," as follows in our ",[22,34582,7575],{},[128,34584,34586],{"className":4024,"code":34585,"language":4026,"meta":133,"style":133},"\u003Cscript setup lang=\"ts\">\n    const htmlString = ref(`\n\u003Ch1>Example Heading\u003C\u002Fh1>\n\u003Cp>Example Paragraph\u003C\u002Fp>\n\u003Ca href=\"\u002Fexample-1\">Example 1\u003C\u002Fa>\n\u003Ca href=\"\u002Fexample-2\">Example 2\u003C\u002Fa>\n\u003Ca href=\"\u002Fexample-3\">Example 3\u003C\u002Fa>`);\n\n    onMounted(() => {\n        setTimeout(() => {\n            htmlString.value = `\n            \u003Ch1>Example Heading\u003C\u002Fh1>\n            \u003Cp>Example Paragraph\u003C\u002Fp>\n            \u003Ca href=\"\u002Fexample-1\">Example 1\u003C\u002Fa>\n            \u003Ca href=\"\u002Fexample-2\">Example 2\u003C\u002Fa>\n            \u003Ca href=\"\u002Fexample-3\">Example 3\u003C\u002Fa>\n            \u003Ca href=\"\u002Fexample-4\">Example 4\u003C\u002Fa>`;\n        }, 4000);\n    });\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv>\n        \u003Cdiv v-nuxt-html=\"htmlString\">\u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,34587,34588,34604,34619,34624,34629,34634,34639,34646,34650,34661,34672,34681,34686,34691,34696,34701,34706,34713,34723,34727,34735,34739,34747,34755,34775,34783],{"__ignoreMap":133},[137,34589,34590,34592,34594,34596,34598,34600,34602],{"class":139,"line":140},[137,34591,4033],{"class":157},[137,34593,4037],{"class":4036},[137,34595,9642],{"class":147},[137,34597,25591],{"class":147},[137,34599,253],{"class":157},[137,34601,29848],{"class":284},[137,34603,4053],{"class":157},[137,34605,34606,34608,34611,34613,34615,34617],{"class":139,"line":173},[137,34607,4177],{"class":143},[137,34609,34610],{"class":364}," htmlString",[137,34612,151],{"class":143},[137,34614,10468],{"class":147},[137,34616,356],{"class":157},[137,34618,22062],{"class":284},[137,34620,34621],{"class":139,"line":188},[137,34622,34623],{"class":284},"\u003Ch1>Example Heading\u003C\u002Fh1>\n",[137,34625,34626],{"class":139,"line":269},[137,34627,34628],{"class":284},"\u003Cp>Example Paragraph\u003C\u002Fp>\n",[137,34630,34631],{"class":139,"line":278},[137,34632,34633],{"class":284},"\u003Ca href=\"\u002Fexample-1\">Example 1\u003C\u002Fa>\n",[137,34635,34636],{"class":139,"line":291},[137,34637,34638],{"class":284},"\u003Ca href=\"\u002Fexample-2\">Example 2\u003C\u002Fa>\n",[137,34640,34641,34644],{"class":139,"line":297},[137,34642,34643],{"class":284},"\u003Ca href=\"\u002Fexample-3\">Example 3\u003C\u002Fa>`",[137,34645,1502],{"class":157},[137,34647,34648],{"class":139,"line":302},[137,34649,516],{"emptyLinePlaceholder":515},[137,34651,34652,34655,34657,34659],{"class":139,"line":662},[137,34653,34654],{"class":147},"    onMounted",[137,34656,3193],{"class":157},[137,34658,222],{"class":143},[137,34660,256],{"class":157},[137,34662,34663,34666,34668,34670],{"class":139,"line":667},[137,34664,34665],{"class":147},"        setTimeout",[137,34667,3193],{"class":157},[137,34669,222],{"class":143},[137,34671,256],{"class":157},[137,34673,34674,34677,34679],{"class":139,"line":786},[137,34675,34676],{"class":157},"            htmlString.value ",[137,34678,253],{"class":143},[137,34680,3778],{"class":284},[137,34682,34683],{"class":139,"line":798},[137,34684,34685],{"class":284},"            \u003Ch1>Example Heading\u003C\u002Fh1>\n",[137,34687,34688],{"class":139,"line":803},[137,34689,34690],{"class":284},"            \u003Cp>Example Paragraph\u003C\u002Fp>\n",[137,34692,34693],{"class":139,"line":931},[137,34694,34695],{"class":284},"            \u003Ca href=\"\u002Fexample-1\">Example 1\u003C\u002Fa>\n",[137,34697,34698],{"class":139,"line":1568},[137,34699,34700],{"class":284},"            \u003Ca href=\"\u002Fexample-2\">Example 2\u003C\u002Fa>\n",[137,34702,34703],{"class":139,"line":1573},[137,34704,34705],{"class":284},"            \u003Ca href=\"\u002Fexample-3\">Example 3\u003C\u002Fa>\n",[137,34707,34708,34711],{"class":139,"line":1578},[137,34709,34710],{"class":284},"            \u003Ca href=\"\u002Fexample-4\">Example 4\u003C\u002Fa>`",[137,34712,3276],{"class":157},[137,34714,34715,34718,34721],{"class":139,"line":1588},[137,34716,34717],{"class":157},"        }, ",[137,34719,34720],{"class":364},"4000",[137,34722,1502],{"class":157},[137,34724,34725],{"class":139,"line":1601},[137,34726,2832],{"class":157},[137,34728,34729,34731,34733],{"class":139,"line":3802},[137,34730,4083],{"class":157},[137,34732,4037],{"class":4036},[137,34734,4053],{"class":157},[137,34736,34737],{"class":139,"line":3808},[137,34738,516],{"emptyLinePlaceholder":515},[137,34740,34741,34743,34745],{"class":139,"line":3822},[137,34742,4033],{"class":157},[137,34744,7821],{"class":4036},[137,34746,4053],{"class":157},[137,34748,34749,34751,34753],{"class":139,"line":3827},[137,34750,4072],{"class":157},[137,34752,8330],{"class":4036},[137,34754,4053],{"class":157},[137,34756,34757,34759,34761,34764,34766,34769,34771,34773],{"class":139,"line":3832},[137,34758,9826],{"class":157},[137,34760,8330],{"class":4036},[137,34762,34763],{"class":147}," v-nuxt-html",[137,34765,253],{"class":157},[137,34767,34768],{"class":284},"\"htmlString\"",[137,34770,4048],{"class":157},[137,34772,8330],{"class":4036},[137,34774,4053],{"class":157},[137,34776,34777,34779,34781],{"class":139,"line":3840},[137,34778,8374],{"class":157},[137,34780,8330],{"class":4036},[137,34782,4053],{"class":157},[137,34784,34785,34787,34789],{"class":139,"line":3846},[137,34786,4083],{"class":157},[137,34788,7821],{"class":4036},[137,34790,4053],{"class":157},[27,34792,34793],{},"Alright, let's break down the code above piece by piece, so we can get a good grasp on how everything fits together.",[1003,34795,34796,34809,34886,34901],{},[1006,34797,34798,34801,34802,34804,34805,34808],{},[42,34799,34800],{},"Dynamic HTML Content:"," The code defines a ",[22,34803,27815],{}," named ",[22,34806,34807],{},"htmlString"," that holds HTML content as a string. This allows for dynamic updates to the displayed HTML.",[1006,34810,34811,34814],{},[42,34812,34813],{},"Initial Render:",[1003,34815,34816,34828,34837],{},[1006,34817,34818,34819,34821,34822,34824,34825,34827],{},"The template section includes a ",[22,34820,8330],{}," element containing another ",[22,34823,8330],{}," with the ",[22,34826,33819],{}," directive applied.",[1006,34829,34830,34831,34833,34834,34836],{},"When the component mounts, the initial value of ",[22,34832,34807],{}," (defined in the ",[22,34835,29134],{}," section) is used.",[1006,34838,34839,34840,34842,34843],{},"The ",[22,34841,33819],{}," directive intercepts this and performs the following actions:\n",[1003,34844,34845,34854,34860],{},[1006,34846,34847,34848,34850,34851,34853],{},"It parses the HTML string using the ",[22,34849,33858],{}," function. This ensures any anchor (",[22,34852,33619],{},") elements without IDs are assigned unique ones.",[1006,34855,34856,34857,34859],{},"It then calls the ",[22,34858,33861],{}," function. This function finds the anchors within the parsed HTML and attaches click event listeners to them.",[1006,34861,34862,34863,34865,34866],{},"When an anchor with a ",[22,34864,29284],{}," attribute is clicked, the event listener:\n",[1003,34867,34868,34871,34880],{},[1006,34869,34870],{},"Prevents default behavior (stopping the browser from navigating away from the current page).",[1006,34872,34873,34874,34876,34877,4409],{},"Extracts the ",[22,34875,29284],{}," attribute value (e.g., ",[22,34878,34879],{},"\u002Fexample-1",[1006,34881,34882,34883,34885],{},"Uses Nuxt's router (",[22,34884,34552],{},") to programmatically navigate to the specified route, effectively turning the anchor into a Nuxt link.",[1006,34887,34888,34891,34892,34894,34895,34897,34898,4409],{},[42,34889,34890],{},"Dynamic Update:"," After a 4-second delay using ",[22,34893,2202],{},", the ",[22,34896,34807],{}," is updated with a new string containing an additional anchor element (",[22,34899,34900],{},"\u003Ca href=\"\u002Fexample-4\">Example 4\u003C\u002Fa>",[1006,34902,34903,34906],{},[42,34904,34905],{},"Re-rendering and Event Listener Management:",[1003,34907,34908,34913,34918],{},[1006,34909,34910,34911,1017],{},"The component re-renders due to the change in ",[22,34912,34807],{},[1006,34914,34839,34915,34917],{},[22,34916,33819],{}," directive again parses and processes the updated HTML.",[1006,34919,34920,34921,34923,34924,34926],{},"Importantly, the directive's ",[22,34922,33851],{}," lifecycle hook ensures any previously attached event listeners are removed using the ",[22,34925,33864],{}," function. This prevents memory leaks and unintended behavior when the HTML content changes.",[104,34928,2567],{"id":2566},[27,34930,34571,34931,34933,34934,34936,34937,34939,34940,34942,34943,34945,34946,1017],{},[22,34932,33576],{}," directive in a Nuxt application has a limitation - it doesn't render ",[22,34935,33572],{},". You can overcome this by creating a custom Vue directive named ",[22,34938,33819],{},". This directive helps convert all ",[22,34941,33619],{}," HTML tags into ",[22,34944,33572],{}," behaviour, enabling dynamic, interactive HTML content in a Nuxt application. The code for this is available in the following GitHub repository ",[45,34947,10647],{"href":34948,"target":2716,"rel":34949},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fnuxt-link-with-v-html",[2718,2719],[2617,34951,34952],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":133,"searchDepth":173,"depth":173,"links":34954},[34955,34956,34958],{"id":33634,"depth":173,"text":33635},{"id":34570,"depth":173,"text":34957},"Using the v-nuxt-html directive",{"id":2566,"depth":173,"text":2567},"If you've used Vue or Nuxt, you're likely familiar with the \"v-html\" directive. This is used to dynamically insert HTML content into an element. It takes a string value containing HTML tags and text, replacing the inner HTML of the target element with this content. One limitation of \"v-html\" is that it does not allow you to render \"\u003Cnuxt-link>\". Instead, all \"\u003Ca>\" HTML tags do not use the native Vue router. However, we can address this limitation by writing our own directive. This will allow all \"\u003Ca>\" tags to be converted into \"\u003Cnuxt-link>\". Thanks to Nuxt and its plugin system, we can easily expand the functionality of Nuxt. Let's get started.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1711259337\u002Fblog\u002FIs%20it%20possible%20to%20use%20nuxt-link%20in%20content%20rendered%20with%20v-html\u002Faktookoffk14wzluq2er",[21096,8,12817,9,34962,33576,34963,34964,34965,5300,5299],"Vue Directives","Nuxt-link","Vue Router","Render html content",{},"\u002F2024\u002F03\u002F24\u002Fis-it-possible-to-use-nuxt-link-in-content-rendered-with-v-html","24th March 2024",{"title":33562,"description":34959},"2024\u002F03\u002F24\u002Fis-it-possible-to-use-nuxt-link-in-content-rendered-with-v-html","6ZU85LG5_rHPbjPi9Zls_9bS1N7IJk99LBDH4twBrsc",{"id":34973,"title":34974,"articleTags":34975,"author":11,"blog":12,"body":34976,"description":36718,"extension":2649,"image":36719,"keywords":36720,"meta":36738,"navigation":515,"path":36739,"published":36740,"readTime":278,"seo":36741,"stem":36742,"type":2662,"__hash__":36743},"content\u002F2025\u002F01\u002F04\u002Funderstanding-javascript-reactivity-with-proxy-and-typescript.md","Understanding JavaScript Reactivity with Proxy and TypeScript",[9,12817,10],{"type":14,"value":34977,"toc":36706},[34978,34981,34995,34997,35001,35006,35013,35017,35022,35215,35225,35232,35238,35477,35480,35486,35498,35732,35735,35739,35748,36070,36073,36077,36086,36645,36649,36679,36681,36693,36696,36703],[17,34979,34974],{"id":34980},"understanding-javascript-reactivity-with-proxy-and-typescript",[27,34982,34983],{},[30,34984,34985,36,34987,40,34989],{},[33,34986],{"value":35},[33,34988],{"value":39},[42,34990,34991],{},[45,34992,34993],{"href":47},[33,34994],{"value":50},[52,34996],{":tags":54},[56,34998],{":audio-src":34999,":transcript-src":35000},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F01\u002F04\u002Funderstanding-javascript-reactivity-with-proxy-and-typescript\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F01\u002F04\u002Funderstanding-javascript-reactivity-with-proxy-and-typescript\u002Fsummary.json",[27,35002,35003],{},[63,35004],{"alt":12847,"src":35005},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1735957390\u002Fblog\u002Funderstanding-javascript-reactivity-with-proxy-and-typescript\u002Fyklh8nkuomwfu8gbmcqq",[27,35007,35008,35009,35012],{},"Reactivity is a core concept in modern JavaScript frameworks like Vue.js, where UI automatically updates when the underlying data changes. But have you ever wondered how to implement a similar reactivity system yourself? In this blog post, we’ll explore how you can use JavaScript's ",[22,35010,35011],{},"Proxy"," to monitor and react to data changes. By the end, we’ll create a simple reactive system with two-way HTML bindings using TypeScript.",[104,35014,35016],{"id":35015},"step-1-monitoring-changes-with-javascript-proxy","Step 1: Monitoring Changes with JavaScript Proxy",[27,35018,4737,35019,35021],{},[22,35020,35011],{}," object in JavaScript allows you to intercept and customise operations performed on an object, such as reading or setting a property. Let’s start with a simple example:",[128,35023,35026],{"className":35024,"code":35025,"language":27871,"meta":133,"style":133},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F Define a target object\nlet target = { value: 0 };\n\n\u002F\u002F Create a Proxy handler to track changes\nconst handler = {\n    set(obj, prop, value) {\n        console.log(`Property \"${prop}\" changed from ${obj[prop]} to ${value}`);\n        obj[prop] = value; \u002F\u002F Update the value\n        return true; \u002F\u002F Indicate success\n    },\n};\n\n\u002F\u002F Create a proxy for the target object\nconst proxy = new Proxy(target, handler);\n\n\u002F\u002F Monitor changes\nproxy.value = 42; \u002F\u002F Logs: Property \"value\" changed from 0 to 42\nproxy.value = 100; \u002F\u002F Logs: Property \"value\" changed from 42 to 100\n",[22,35027,35028,35033,35049,35053,35058,35068,35087,35120,35133,35144,35148,35152,35156,35161,35178,35182,35187,35202],{"__ignoreMap":133},[137,35029,35030],{"class":139,"line":140},[137,35031,35032],{"class":308},"\u002F\u002F Define a target object\n",[137,35034,35035,35037,35040,35042,35045,35047],{"class":139,"line":173},[137,35036,11498],{"class":143},[137,35038,35039],{"class":157}," target ",[137,35041,253],{"class":143},[137,35043,35044],{"class":157}," { value: ",[137,35046,6044],{"class":364},[137,35048,32107],{"class":157},[137,35050,35051],{"class":139,"line":188},[137,35052,516],{"emptyLinePlaceholder":515},[137,35054,35055],{"class":139,"line":269},[137,35056,35057],{"class":308},"\u002F\u002F Create a Proxy handler to track changes\n",[137,35059,35060,35062,35064,35066],{"class":139,"line":278},[137,35061,3077],{"class":143},[137,35063,33135],{"class":364},[137,35065,151],{"class":143},[137,35067,256],{"class":157},[137,35069,35070,35072,35074,35076,35078,35081,35083,35085],{"class":139,"line":291},[137,35071,5409],{"class":147},[137,35073,356],{"class":157},[137,35075,990],{"class":161},[137,35077,164],{"class":157},[137,35079,35080],{"class":161},"prop",[137,35082,164],{"class":157},[137,35084,5414],{"class":161},[137,35086,170],{"class":157},[137,35088,35089,35091,35093,35095,35098,35100,35103,35105,35107,35109,35112,35114,35116,35118],{"class":139,"line":297},[137,35090,350],{"class":157},[137,35092,353],{"class":147},[137,35094,356],{"class":157},[137,35096,35097],{"class":284},"`Property \"${",[137,35099,35080],{"class":157},[137,35101,35102],{"class":284},"}\" changed from ${",[137,35104,990],{"class":157},[137,35106,5717],{"class":284},[137,35108,35080],{"class":157},[137,35110,35111],{"class":284},"]",[137,35113,5559],{"class":284},[137,35115,5414],{"class":157},[137,35117,4706],{"class":284},[137,35119,1502],{"class":157},[137,35121,35122,35125,35127,35130],{"class":139,"line":302},[137,35123,35124],{"class":157},"        obj[prop] ",[137,35126,253],{"class":143},[137,35128,35129],{"class":157}," value; ",[137,35131,35132],{"class":308},"\u002F\u002F Update the value\n",[137,35134,35135,35137,35139,35141],{"class":139,"line":662},[137,35136,5472],{"class":143},[137,35138,14286],{"class":364},[137,35140,2323],{"class":157},[137,35142,35143],{"class":308},"\u002F\u002F Indicate success\n",[137,35145,35146],{"class":139,"line":667},[137,35147,775],{"class":157},[137,35149,35150],{"class":139,"line":786},[137,35151,191],{"class":157},[137,35153,35154],{"class":139,"line":798},[137,35155,516],{"emptyLinePlaceholder":515},[137,35157,35158],{"class":139,"line":803},[137,35159,35160],{"class":308},"\u002F\u002F Create a proxy for the target object\n",[137,35162,35163,35165,35168,35170,35172,35175],{"class":139,"line":931},[137,35164,3077],{"class":143},[137,35166,35167],{"class":364}," proxy",[137,35169,151],{"class":143},[137,35171,1426],{"class":143},[137,35173,35174],{"class":147}," Proxy",[137,35176,35177],{"class":157},"(target, handler);\n",[137,35179,35180],{"class":139,"line":1568},[137,35181,516],{"emptyLinePlaceholder":515},[137,35183,35184],{"class":139,"line":1573},[137,35185,35186],{"class":308},"\u002F\u002F Monitor changes\n",[137,35188,35189,35192,35194,35197,35199],{"class":139,"line":1578},[137,35190,35191],{"class":157},"proxy.value ",[137,35193,253],{"class":143},[137,35195,35196],{"class":364}," 42",[137,35198,2323],{"class":157},[137,35200,35201],{"class":308},"\u002F\u002F Logs: Property \"value\" changed from 0 to 42\n",[137,35203,35204,35206,35208,35210,35212],{"class":139,"line":1588},[137,35205,35191],{"class":157},[137,35207,253],{"class":143},[137,35209,12500],{"class":364},[137,35211,2323],{"class":157},[137,35213,35214],{"class":308},"\u002F\u002F Logs: Property \"value\" changed from 42 to 100\n",[27,35216,35217,35218,35220,35221,35224],{},"Here, every time the ",[22,35219,5414],{}," property changes, the ",[22,35222,35223],{},"set"," trap is triggered, and we log the change. This is the foundation for building a reactive system.",[104,35226,35228,35229,35231],{"id":35227},"step-2-creating-a-reusable-watch-function","Step 2: Creating a Reusable ",[22,35230,27791],{}," Function",[27,35233,35234,35235,35237],{},"Next, let’s create a reusable function called ",[22,35236,27791],{},". This function will monitor changes to a specific property and execute a callback whenever the property changes.",[128,35239,35241],{"className":35024,"code":35240,"language":27871,"meta":133,"style":133},"function watch(target, property, callback) {\n    const handler = {\n        set(obj, prop, value) {\n            if (prop === property) {\n                callback(value, obj[prop]); \u002F\u002F Call the callback\n            }\n            obj[prop] = value; \u002F\u002F Update the value\n            return true;\n        },\n    };\n\n    return new Proxy(target, handler);\n}\n\n\u002F\u002F Usage example\nlet data = { value: 0 };\nconst watchedData = watch(data, \"value\", (newValue, oldValue) => {\n    console.log(`Value changed from ${oldValue} to ${newValue}`);\n});\n\nwatchedData.value = 42; \u002F\u002F Logs: Value changed from 0 to 42\nwatchedData.value = 100; \u002F\u002F Logs: Value changed from 42 to 100\n",[22,35242,35243,35267,35277,35295,35307,35318,35322,35333,35341,35345,35349,35353,35363,35367,35371,35376,35390,35421,35442,35446,35450,35464],{"__ignoreMap":133},[137,35244,35245,35247,35250,35252,35255,35257,35260,35262,35265],{"class":139,"line":140},[137,35246,483],{"class":143},[137,35248,35249],{"class":147}," watch",[137,35251,356],{"class":157},[137,35253,35254],{"class":161},"target",[137,35256,164],{"class":157},[137,35258,35259],{"class":161},"property",[137,35261,164],{"class":157},[137,35263,35264],{"class":161},"callback",[137,35266,170],{"class":157},[137,35268,35269,35271,35273,35275],{"class":139,"line":173},[137,35270,4177],{"class":143},[137,35272,33135],{"class":364},[137,35274,151],{"class":143},[137,35276,256],{"class":157},[137,35278,35279,35281,35283,35285,35287,35289,35291,35293],{"class":139,"line":188},[137,35280,5736],{"class":147},[137,35282,356],{"class":157},[137,35284,990],{"class":161},[137,35286,164],{"class":157},[137,35288,35080],{"class":161},[137,35290,164],{"class":157},[137,35292,5414],{"class":161},[137,35294,170],{"class":157},[137,35296,35297,35299,35302,35304],{"class":139,"line":269},[137,35298,5747],{"class":143},[137,35300,35301],{"class":157}," (prop ",[137,35303,5502],{"class":143},[137,35305,35306],{"class":157}," property) {\n",[137,35308,35309,35312,35315],{"class":139,"line":278},[137,35310,35311],{"class":147},"                callback",[137,35313,35314],{"class":157},"(value, obj[prop]); ",[137,35316,35317],{"class":308},"\u002F\u002F Call the callback\n",[137,35319,35320],{"class":139,"line":291},[137,35321,760],{"class":157},[137,35323,35324,35327,35329,35331],{"class":139,"line":297},[137,35325,35326],{"class":157},"            obj[prop] ",[137,35328,253],{"class":143},[137,35330,35129],{"class":157},[137,35332,35132],{"class":308},[137,35334,35335,35337,35339],{"class":139,"line":302},[137,35336,4683],{"class":143},[137,35338,14286],{"class":364},[137,35340,3276],{"class":157},[137,35342,35343],{"class":139,"line":662},[137,35344,2084],{"class":157},[137,35346,35347],{"class":139,"line":667},[137,35348,1892],{"class":157},[137,35350,35351],{"class":139,"line":786},[137,35352,516],{"emptyLinePlaceholder":515},[137,35354,35355,35357,35359,35361],{"class":139,"line":798},[137,35356,176],{"class":143},[137,35358,1426],{"class":143},[137,35360,35174],{"class":147},[137,35362,35177],{"class":157},[137,35364,35365],{"class":139,"line":803},[137,35366,510],{"class":157},[137,35368,35369],{"class":139,"line":931},[137,35370,516],{"emptyLinePlaceholder":515},[137,35372,35373],{"class":139,"line":1568},[137,35374,35375],{"class":308},"\u002F\u002F Usage example\n",[137,35377,35378,35380,35382,35384,35386,35388],{"class":139,"line":1573},[137,35379,11498],{"class":143},[137,35381,20443],{"class":157},[137,35383,253],{"class":143},[137,35385,35044],{"class":157},[137,35387,6044],{"class":364},[137,35389,32107],{"class":157},[137,35391,35392,35394,35397,35399,35401,35404,35407,35409,35411,35413,35415,35417,35419],{"class":139,"line":1578},[137,35393,3077],{"class":143},[137,35395,35396],{"class":364}," watchedData",[137,35398,151],{"class":143},[137,35400,35249],{"class":147},[137,35402,35403],{"class":157},"(data, ",[137,35405,35406],{"class":284},"\"value\"",[137,35408,24531],{"class":157},[137,35410,6125],{"class":161},[137,35412,164],{"class":157},[137,35414,5432],{"class":161},[137,35416,219],{"class":157},[137,35418,222],{"class":143},[137,35420,256],{"class":157},[137,35422,35423,35425,35427,35429,35432,35434,35436,35438,35440],{"class":139,"line":1588},[137,35424,493],{"class":157},[137,35426,353],{"class":147},[137,35428,356],{"class":157},[137,35430,35431],{"class":284},"`Value changed from ${",[137,35433,5432],{"class":157},[137,35435,5559],{"class":284},[137,35437,6125],{"class":157},[137,35439,4706],{"class":284},[137,35441,1502],{"class":157},[137,35443,35444],{"class":139,"line":1601},[137,35445,5422],{"class":157},[137,35447,35448],{"class":139,"line":3802},[137,35449,516],{"emptyLinePlaceholder":515},[137,35451,35452,35455,35457,35459,35461],{"class":139,"line":3808},[137,35453,35454],{"class":157},"watchedData.value ",[137,35456,253],{"class":143},[137,35458,35196],{"class":364},[137,35460,2323],{"class":157},[137,35462,35463],{"class":308},"\u002F\u002F Logs: Value changed from 0 to 42\n",[137,35465,35466,35468,35470,35472,35474],{"class":139,"line":3822},[137,35467,35454],{"class":157},[137,35469,253],{"class":143},[137,35471,12500],{"class":364},[137,35473,2323],{"class":157},[137,35475,35476],{"class":308},"\u002F\u002F Logs: Value changed from 42 to 100\n",[27,35478,35479],{},"This abstraction makes it easy to monitor changes for any property in any object.",[104,35481,35483,35484],{"id":35482},"step-3-simplifying-reactivity-with-ref","Step 3: Simplifying Reactivity with ",[22,35485,27815],{},[27,35487,35488,35489,35491,35492,35494,35495,35497],{},"Inspired by Vue.js, we can simplify our system by wrapping a value in a ",[22,35490,27815],{},". A ",[22,35493,27815],{}," is an object with a single ",[22,35496,5414],{}," property that is reactive. Here’s how it looks in JavaScript:",[128,35499,35501],{"className":35024,"code":35500,"language":27871,"meta":133,"style":133},"function ref(initialValue, onChange) {\n    const target = { value: initialValue };\n\n    const handler = {\n        set(obj, prop, newValue) {\n            if (prop === \"value\") {\n                onChange?.(newValue, obj[prop]); \u002F\u002F Trigger callback\n            }\n            obj[prop] = newValue; \u002F\u002F Update the value\n            return true;\n        },\n    };\n\n    return new Proxy(target, handler);\n}\n\n\u002F\u002F Example usage\nconst name = ref(\"Alex\", (newValue, oldValue) => {\n    console.log(`Name changed from ${oldValue} to ${newValue}`);\n});\n\nconsole.log(name.value); \u002F\u002F Alex\nname.value = \"John\"; \u002F\u002F Logs: Name changed from Alex to John\n",[22,35502,35503,35521,35533,35537,35547,35565,35578,35589,35593,35604,35612,35616,35620,35624,35634,35638,35642,35647,35676,35697,35701,35705,35717],{"__ignoreMap":133},[137,35504,35505,35507,35509,35511,35514,35516,35519],{"class":139,"line":140},[137,35506,483],{"class":143},[137,35508,10468],{"class":147},[137,35510,356],{"class":157},[137,35512,35513],{"class":161},"initialValue",[137,35515,164],{"class":157},[137,35517,35518],{"class":161},"onChange",[137,35520,170],{"class":157},[137,35522,35523,35525,35528,35530],{"class":139,"line":173},[137,35524,4177],{"class":143},[137,35526,35527],{"class":364}," target",[137,35529,151],{"class":143},[137,35531,35532],{"class":157}," { value: initialValue };\n",[137,35534,35535],{"class":139,"line":188},[137,35536,516],{"emptyLinePlaceholder":515},[137,35538,35539,35541,35543,35545],{"class":139,"line":269},[137,35540,4177],{"class":143},[137,35542,33135],{"class":364},[137,35544,151],{"class":143},[137,35546,256],{"class":157},[137,35548,35549,35551,35553,35555,35557,35559,35561,35563],{"class":139,"line":278},[137,35550,5736],{"class":147},[137,35552,356],{"class":157},[137,35554,990],{"class":161},[137,35556,164],{"class":157},[137,35558,35080],{"class":161},[137,35560,164],{"class":157},[137,35562,6125],{"class":161},[137,35564,170],{"class":157},[137,35566,35567,35569,35571,35573,35576],{"class":139,"line":291},[137,35568,5747],{"class":143},[137,35570,35301],{"class":157},[137,35572,5502],{"class":143},[137,35574,35575],{"class":284}," \"value\"",[137,35577,170],{"class":157},[137,35579,35580,35583,35586],{"class":139,"line":297},[137,35581,35582],{"class":147},"                onChange",[137,35584,35585],{"class":157},"?.(newValue, obj[prop]); ",[137,35587,35588],{"class":308},"\u002F\u002F Trigger callback\n",[137,35590,35591],{"class":139,"line":302},[137,35592,760],{"class":157},[137,35594,35595,35597,35599,35602],{"class":139,"line":662},[137,35596,35326],{"class":157},[137,35598,253],{"class":143},[137,35600,35601],{"class":157}," newValue; ",[137,35603,35132],{"class":308},[137,35605,35606,35608,35610],{"class":139,"line":667},[137,35607,4683],{"class":143},[137,35609,14286],{"class":364},[137,35611,3276],{"class":157},[137,35613,35614],{"class":139,"line":786},[137,35615,2084],{"class":157},[137,35617,35618],{"class":139,"line":798},[137,35619,1892],{"class":157},[137,35621,35622],{"class":139,"line":803},[137,35623,516],{"emptyLinePlaceholder":515},[137,35625,35626,35628,35630,35632],{"class":139,"line":931},[137,35627,176],{"class":143},[137,35629,1426],{"class":143},[137,35631,35174],{"class":147},[137,35633,35177],{"class":157},[137,35635,35636],{"class":139,"line":1568},[137,35637,510],{"class":157},[137,35639,35640],{"class":139,"line":1573},[137,35641,516],{"emptyLinePlaceholder":515},[137,35643,35644],{"class":139,"line":1578},[137,35645,35646],{"class":308},"\u002F\u002F Example usage\n",[137,35648,35649,35651,35653,35655,35657,35659,35662,35664,35666,35668,35670,35672,35674],{"class":139,"line":1588},[137,35650,3077],{"class":143},[137,35652,891],{"class":364},[137,35654,151],{"class":143},[137,35656,10468],{"class":147},[137,35658,356],{"class":157},[137,35660,35661],{"class":284},"\"Alex\"",[137,35663,24531],{"class":157},[137,35665,6125],{"class":161},[137,35667,164],{"class":157},[137,35669,5432],{"class":161},[137,35671,219],{"class":157},[137,35673,222],{"class":143},[137,35675,256],{"class":157},[137,35677,35678,35680,35682,35684,35687,35689,35691,35693,35695],{"class":139,"line":1601},[137,35679,493],{"class":157},[137,35681,353],{"class":147},[137,35683,356],{"class":157},[137,35685,35686],{"class":284},"`Name changed from ${",[137,35688,5432],{"class":157},[137,35690,5559],{"class":284},[137,35692,6125],{"class":157},[137,35694,4706],{"class":284},[137,35696,1502],{"class":157},[137,35698,35699],{"class":139,"line":3802},[137,35700,5422],{"class":157},[137,35702,35703],{"class":139,"line":3808},[137,35704,516],{"emptyLinePlaceholder":515},[137,35706,35707,35709,35711,35714],{"class":139,"line":3822},[137,35708,1436],{"class":157},[137,35710,353],{"class":147},[137,35712,35713],{"class":157},"(name.value); ",[137,35715,35716],{"class":308},"\u002F\u002F Alex\n",[137,35718,35719,35722,35724,35727,35729],{"class":139,"line":3827},[137,35720,35721],{"class":157},"name.value ",[137,35723,253],{"class":143},[137,35725,35726],{"class":284}," \"John\"",[137,35728,2323],{"class":157},[137,35730,35731],{"class":308},"\u002F\u002F Logs: Name changed from Alex to John\n",[27,35733,35734],{},"This implementation is simple and reusable, but we can make it even better by adding type safety with TypeScript.",[104,35736,35738],{"id":35737},"step-4-adding-typescript-for-type-safety","Step 4: Adding TypeScript for Type Safety",[27,35740,35741,35742,35744,35745,35747],{},"With TypeScript, we can make the ",[22,35743,27815],{}," function type-safe. This ensures that the ",[22,35746,5414],{}," property always has the correct type and the callback receives properly typed arguments.",[128,35749,35752],{"className":35750,"code":35751,"language":27873,"meta":133,"style":133},"language-typescript shiki shiki-themes github-light github-dark","type RefCallback\u003CT> = (newValue: T, oldValue: T) => void;\n\nfunction ref\u003CT>(initialValue: T, onChange?: RefCallback\u003CT>) {\n    const target = { value: initialValue };\n\n    const handler: ProxyHandler\u003C{ value: T }> = {\n        set(obj, prop, newValue) {\n            if (prop === \"value\" && obj.value !== newValue) {\n                onChange?.(newValue as T, obj.value); \u002F\u002F Trigger callback\n            }\n            obj[prop as keyof typeof obj] = newValue;\n            return true;\n        },\n    };\n\n    return new Proxy(target, handler);\n}\n\n\u002F\u002F Usage with TypeScript\nconst age = ref(25, (newValue, oldValue) => {\n    console.log(`Age changed from ${oldValue} to ${newValue}`);\n});\n\nage.value = 30; \u002F\u002F Logs: Age changed from 25 to 30\n",[22,35753,35754,35796,35800,35834,35844,35848,35875,35893,35914,35930,35934,35954,35962,35966,35970,35974,35984,35988,35992,35997,36026,36047,36051,36055],{"__ignoreMap":133},[137,35755,35756,35758,35761,35763,35766,35768,35770,35772,35774,35776,35779,35781,35783,35785,35787,35789,35791,35794],{"class":139,"line":140},[137,35757,20355],{"class":143},[137,35759,35760],{"class":147}," RefCallback",[137,35762,4033],{"class":157},[137,35764,35765],{"class":147},"T",[137,35767,14124],{"class":157},[137,35769,253],{"class":143},[137,35771,158],{"class":157},[137,35773,6125],{"class":161},[137,35775,894],{"class":143},[137,35777,35778],{"class":147}," T",[137,35780,164],{"class":157},[137,35782,5432],{"class":161},[137,35784,894],{"class":143},[137,35786,35778],{"class":147},[137,35788,219],{"class":157},[137,35790,222],{"class":143},[137,35792,35793],{"class":364}," void",[137,35795,3276],{"class":157},[137,35797,35798],{"class":139,"line":173},[137,35799,516],{"emptyLinePlaceholder":515},[137,35801,35802,35804,35806,35808,35810,35812,35814,35816,35818,35820,35822,35825,35827,35829,35831],{"class":139,"line":188},[137,35803,483],{"class":143},[137,35805,10468],{"class":147},[137,35807,4033],{"class":157},[137,35809,35765],{"class":147},[137,35811,26976],{"class":157},[137,35813,35513],{"class":161},[137,35815,894],{"class":143},[137,35817,35778],{"class":147},[137,35819,164],{"class":157},[137,35821,35518],{"class":161},[137,35823,35824],{"class":143},"?:",[137,35826,35760],{"class":147},[137,35828,4033],{"class":157},[137,35830,35765],{"class":147},[137,35832,35833],{"class":157},">) {\n",[137,35835,35836,35838,35840,35842],{"class":139,"line":269},[137,35837,4177],{"class":143},[137,35839,35527],{"class":364},[137,35841,151],{"class":143},[137,35843,35532],{"class":157},[137,35845,35846],{"class":139,"line":278},[137,35847,516],{"emptyLinePlaceholder":515},[137,35849,35850,35852,35854,35856,35859,35862,35864,35866,35868,35871,35873],{"class":139,"line":291},[137,35851,4177],{"class":143},[137,35853,33135],{"class":364},[137,35855,894],{"class":143},[137,35857,35858],{"class":147}," ProxyHandler",[137,35860,35861],{"class":157},"\u003C{ ",[137,35863,5414],{"class":161},[137,35865,894],{"class":143},[137,35867,35778],{"class":147},[137,35869,35870],{"class":157}," }> ",[137,35872,253],{"class":143},[137,35874,256],{"class":157},[137,35876,35877,35879,35881,35883,35885,35887,35889,35891],{"class":139,"line":297},[137,35878,5736],{"class":147},[137,35880,356],{"class":157},[137,35882,990],{"class":161},[137,35884,164],{"class":157},[137,35886,35080],{"class":161},[137,35888,164],{"class":157},[137,35890,6125],{"class":161},[137,35892,170],{"class":157},[137,35894,35895,35897,35899,35901,35903,35906,35909,35911],{"class":139,"line":302},[137,35896,5747],{"class":143},[137,35898,35301],{"class":157},[137,35900,5502],{"class":143},[137,35902,35575],{"class":284},[137,35904,35905],{"class":143}," &&",[137,35907,35908],{"class":157}," obj.value ",[137,35910,26215],{"class":143},[137,35912,35913],{"class":157}," newValue) {\n",[137,35915,35916,35918,35921,35923,35925,35928],{"class":139,"line":662},[137,35917,35582],{"class":147},[137,35919,35920],{"class":157},"?.(newValue ",[137,35922,24431],{"class":143},[137,35924,35778],{"class":147},[137,35926,35927],{"class":157},", obj.value); ",[137,35929,35588],{"class":308},[137,35931,35932],{"class":139,"line":667},[137,35933,760],{"class":157},[137,35935,35936,35939,35941,35943,35946,35949,35951],{"class":139,"line":786},[137,35937,35938],{"class":157},"            obj[prop ",[137,35940,24431],{"class":143},[137,35942,20363],{"class":143},[137,35944,35945],{"class":143}," typeof",[137,35947,35948],{"class":157}," obj] ",[137,35950,253],{"class":143},[137,35952,35953],{"class":157}," newValue;\n",[137,35955,35956,35958,35960],{"class":139,"line":798},[137,35957,4683],{"class":143},[137,35959,14286],{"class":364},[137,35961,3276],{"class":157},[137,35963,35964],{"class":139,"line":803},[137,35965,2084],{"class":157},[137,35967,35968],{"class":139,"line":931},[137,35969,1892],{"class":157},[137,35971,35972],{"class":139,"line":1568},[137,35973,516],{"emptyLinePlaceholder":515},[137,35975,35976,35978,35980,35982],{"class":139,"line":1573},[137,35977,176],{"class":143},[137,35979,1426],{"class":143},[137,35981,35174],{"class":147},[137,35983,35177],{"class":157},[137,35985,35986],{"class":139,"line":1578},[137,35987,510],{"class":157},[137,35989,35990],{"class":139,"line":1588},[137,35991,516],{"emptyLinePlaceholder":515},[137,35993,35994],{"class":139,"line":1601},[137,35995,35996],{"class":308},"\u002F\u002F Usage with TypeScript\n",[137,35998,35999,36001,36003,36005,36007,36009,36012,36014,36016,36018,36020,36022,36024],{"class":139,"line":3802},[137,36000,3077],{"class":143},[137,36002,1152],{"class":364},[137,36004,151],{"class":143},[137,36006,10468],{"class":147},[137,36008,356],{"class":157},[137,36010,36011],{"class":364},"25",[137,36013,24531],{"class":157},[137,36015,6125],{"class":161},[137,36017,164],{"class":157},[137,36019,5432],{"class":161},[137,36021,219],{"class":157},[137,36023,222],{"class":143},[137,36025,256],{"class":157},[137,36027,36028,36030,36032,36034,36037,36039,36041,36043,36045],{"class":139,"line":3808},[137,36029,493],{"class":157},[137,36031,353],{"class":147},[137,36033,356],{"class":157},[137,36035,36036],{"class":284},"`Age changed from ${",[137,36038,5432],{"class":157},[137,36040,5559],{"class":284},[137,36042,6125],{"class":157},[137,36044,4706],{"class":284},[137,36046,1502],{"class":157},[137,36048,36049],{"class":139,"line":3822},[137,36050,5422],{"class":157},[137,36052,36053],{"class":139,"line":3827},[137,36054,516],{"emptyLinePlaceholder":515},[137,36056,36057,36060,36062,36065,36067],{"class":139,"line":3832},[137,36058,36059],{"class":157},"age.value ",[137,36061,253],{"class":143},[137,36063,36064],{"class":364}," 30",[137,36066,2323],{"class":157},[137,36068,36069],{"class":308},"\u002F\u002F Logs: Age changed from 25 to 30\n",[27,36071,36072],{},"With TypeScript, you get type safety, better IDE support, and fewer runtime errors.",[104,36074,36076],{"id":36075},"step-5-building-a-reactive-html-system","Step 5: Building a Reactive HTML System",[27,36078,36079,36080,36082,36083,36085],{},"Finally, let’s create a system where changes to a ",[22,36081,27815],{}," update the DOM reactively. We’ll use our ",[22,36084,27815],{}," implementation from the previous step.",[128,36087,36089],{"className":4024,"code":36088,"language":4026,"meta":133,"style":133},"\u003C!doctype html>\n\u003Chtml lang=\"en\">\n    \u003Chead>\n        \u003Cmeta charset=\"UTF-8\" \u002F>\n        \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" \u002F>\n        \u003Ctitle>Reactivity with Proxy and TypeScript\u003C\u002Ftitle>\n    \u003C\u002Fhead>\n    \u003Cbody>\n        \u003Cdiv>\n            \u003Cp id=\"text\">\u003C\u002Fp>\n            \u003Cinput id=\"input\" type=\"text\" placeholder=\"Enter text...\" \u002F>\n        \u003C\u002Fdiv>\n        \u003Cscript>\n            function ref(initialValue, onChange) {\n                const target = { value: initialValue };\n\n                const handler = {\n                    set(obj, prop, newValue) {\n                        if (prop === \"value\" && obj.value !== newValue) {\n                            onChange?.(newValue, obj.value); \u002F\u002F Trigger callback\n                        }\n                        obj[prop] = newValue;\n                        return true;\n                    },\n                };\n\n                return new Proxy(target, handler);\n            }\n\n            document.body.innerHTML = `\n          \u003Cdiv>\n            \u003Cp id=\"text\">\u003C\u002Fp>\n            \u003Cinput id=\"input\" type=\"text\" placeholder=\"Enter text...\" \u002F>\n          \u003C\u002Fdiv>\n        `;\n\n            const textElement = document.getElementById(\"text\");\n            const inputElement = document.getElementById(\"input\");\n\n            \u002F\u002F Create a reactive ref\n            const name = ref(\"Alex\", (newValue) => {\n                textElement.textContent = `Hello, ${newValue}!`; \u002F\u002F Update the DOM reactively\n            });\n\n            \u002F\u002F Initialize the DOM\n            textElement.textContent = `Hello, ${name.value}!`;\n\n            \u002F\u002F Listen for input changes\n            inputElement.addEventListener(\"input\", (event) => {\n                const target = event.target;\n                name.value = target.value; \u002F\u002F Trigger reactivity\n            });\n        \u003C\u002Fscript>\n    \u003C\u002Fbody>\n\u003C\u002Fhtml>\n",[22,36090,36091,36101,36115,36123,36137,36157,36170,36178,36186,36194,36212,36241,36249,36257,36273,36283,36287,36297,36316,36335,36345,36350,36359,36368,36373,36378,36382,36392,36396,36400,36409,36414,36419,36424,36429,36436,36440,36459,36478,36482,36487,36511,36531,36535,36539,36544,36563,36567,36572,36593,36604,36617,36621,36629,36637],{"__ignoreMap":133},[137,36092,36093,36095,36097,36099],{"class":139,"line":140},[137,36094,25574],{"class":157},[137,36096,25577],{"class":4036},[137,36098,25580],{"class":147},[137,36100,4053],{"class":157},[137,36102,36103,36105,36107,36109,36111,36113],{"class":139,"line":173},[137,36104,4033],{"class":157},[137,36106,4026],{"class":4036},[137,36108,25591],{"class":147},[137,36110,253],{"class":157},[137,36112,25596],{"class":284},[137,36114,4053],{"class":157},[137,36116,36117,36119,36121],{"class":139,"line":188},[137,36118,4072],{"class":157},[137,36120,25605],{"class":4036},[137,36122,4053],{"class":157},[137,36124,36125,36127,36129,36131,36133,36135],{"class":139,"line":269},[137,36126,9826],{"class":157},[137,36128,23508],{"class":4036},[137,36130,25616],{"class":147},[137,36132,253],{"class":157},[137,36134,25621],{"class":284},[137,36136,4078],{"class":157},[137,36138,36139,36141,36143,36145,36147,36149,36151,36153,36155],{"class":139,"line":278},[137,36140,9826],{"class":157},[137,36142,23508],{"class":4036},[137,36144,891],{"class":147},[137,36146,253],{"class":157},[137,36148,25666],{"class":284},[137,36150,25669],{"class":147},[137,36152,253],{"class":157},[137,36154,25674],{"class":284},[137,36156,4078],{"class":157},[137,36158,36159,36161,36163,36166,36168],{"class":139,"line":291},[137,36160,9826],{"class":157},[137,36162,25683],{"class":4036},[137,36164,36165],{"class":157},">Reactivity with Proxy and TypeScript\u003C\u002F",[137,36167,25683],{"class":4036},[137,36169,4053],{"class":157},[137,36171,36172,36174,36176],{"class":139,"line":297},[137,36173,8374],{"class":157},[137,36175,25605],{"class":4036},[137,36177,4053],{"class":157},[137,36179,36180,36182,36184],{"class":139,"line":302},[137,36181,4072],{"class":157},[137,36183,4065],{"class":4036},[137,36185,4053],{"class":157},[137,36187,36188,36190,36192],{"class":139,"line":662},[137,36189,9826],{"class":157},[137,36191,8330],{"class":4036},[137,36193,4053],{"class":157},[137,36195,36196,36198,36200,36202,36204,36206,36208,36210],{"class":139,"line":667},[137,36197,23852],{"class":157},[137,36199,27],{"class":4036},[137,36201,23757],{"class":147},[137,36203,253],{"class":157},[137,36205,7837],{"class":284},[137,36207,4048],{"class":157},[137,36209,27],{"class":4036},[137,36211,4053],{"class":157},[137,36213,36214,36216,36218,36220,36222,36225,36227,36229,36231,36234,36236,36239],{"class":139,"line":786},[137,36215,23852],{"class":157},[137,36217,8520],{"class":4036},[137,36219,23757],{"class":147},[137,36221,253],{"class":157},[137,36223,36224],{"class":284},"\"input\"",[137,36226,25639],{"class":147},[137,36228,253],{"class":157},[137,36230,7837],{"class":284},[137,36232,36233],{"class":147}," placeholder",[137,36235,253],{"class":157},[137,36237,36238],{"class":284},"\"Enter text...\"",[137,36240,4078],{"class":157},[137,36242,36243,36245,36247],{"class":139,"line":798},[137,36244,9843],{"class":157},[137,36246,8330],{"class":4036},[137,36248,4053],{"class":157},[137,36250,36251,36253,36255],{"class":139,"line":803},[137,36252,9826],{"class":157},[137,36254,4037],{"class":4036},[137,36256,4053],{"class":157},[137,36258,36259,36261,36263,36265,36267,36269,36271],{"class":139,"line":931},[137,36260,735],{"class":143},[137,36262,10468],{"class":147},[137,36264,356],{"class":157},[137,36266,35513],{"class":161},[137,36268,164],{"class":157},[137,36270,35518],{"class":161},[137,36272,170],{"class":157},[137,36274,36275,36277,36279,36281],{"class":139,"line":1568},[137,36276,34330],{"class":143},[137,36278,35527],{"class":364},[137,36280,151],{"class":143},[137,36282,35532],{"class":157},[137,36284,36285],{"class":139,"line":1573},[137,36286,516],{"emptyLinePlaceholder":515},[137,36288,36289,36291,36293,36295],{"class":139,"line":1578},[137,36290,34330],{"class":143},[137,36292,33135],{"class":364},[137,36294,151],{"class":143},[137,36296,256],{"class":157},[137,36298,36299,36302,36304,36306,36308,36310,36312,36314],{"class":139,"line":1588},[137,36300,36301],{"class":147},"                    set",[137,36303,356],{"class":157},[137,36305,990],{"class":161},[137,36307,164],{"class":157},[137,36309,35080],{"class":161},[137,36311,164],{"class":157},[137,36313,6125],{"class":161},[137,36315,170],{"class":157},[137,36317,36318,36321,36323,36325,36327,36329,36331,36333],{"class":139,"line":1601},[137,36319,36320],{"class":143},"                        if",[137,36322,35301],{"class":157},[137,36324,5502],{"class":143},[137,36326,35575],{"class":284},[137,36328,35905],{"class":143},[137,36330,35908],{"class":157},[137,36332,26215],{"class":143},[137,36334,35913],{"class":157},[137,36336,36337,36340,36343],{"class":139,"line":3802},[137,36338,36339],{"class":147},"                            onChange",[137,36341,36342],{"class":157},"?.(newValue, obj.value); ",[137,36344,35588],{"class":308},[137,36346,36347],{"class":139,"line":3808},[137,36348,36349],{"class":157},"                        }\n",[137,36351,36352,36355,36357],{"class":139,"line":3822},[137,36353,36354],{"class":157},"                        obj[prop] ",[137,36356,253],{"class":143},[137,36358,35953],{"class":157},[137,36360,36361,36364,36366],{"class":139,"line":3827},[137,36362,36363],{"class":143},"                        return",[137,36365,14286],{"class":364},[137,36367,3276],{"class":157},[137,36369,36370],{"class":139,"line":3832},[137,36371,36372],{"class":157},"                    },\n",[137,36374,36375],{"class":139,"line":3840},[137,36376,36377],{"class":157},"                };\n",[137,36379,36380],{"class":139,"line":3846},[137,36381,516],{"emptyLinePlaceholder":515},[137,36383,36384,36386,36388,36390],{"class":139,"line":3861},[137,36385,5761],{"class":143},[137,36387,1426],{"class":143},[137,36389,35174],{"class":147},[137,36391,35177],{"class":157},[137,36393,36394],{"class":139,"line":3883},[137,36395,760],{"class":157},[137,36397,36398],{"class":139,"line":3896},[137,36399,516],{"emptyLinePlaceholder":515},[137,36401,36402,36405,36407],{"class":139,"line":3901},[137,36403,36404],{"class":157},"            document.body.innerHTML ",[137,36406,253],{"class":143},[137,36408,3778],{"class":284},[137,36410,36411],{"class":139,"line":3906},[137,36412,36413],{"class":284},"          \u003Cdiv>\n",[137,36415,36416],{"class":139,"line":3911},[137,36417,36418],{"class":284},"            \u003Cp id=\"text\">\u003C\u002Fp>\n",[137,36420,36421],{"class":139,"line":4666},[137,36422,36423],{"class":284},"            \u003Cinput id=\"input\" type=\"text\" placeholder=\"Enter text...\" \u002F>\n",[137,36425,36426],{"class":139,"line":4672},[137,36427,36428],{"class":284},"          \u003C\u002Fdiv>\n",[137,36430,36431,36434],{"class":139,"line":4680},[137,36432,36433],{"class":284},"        `",[137,36435,3276],{"class":157},[137,36437,36438],{"class":139,"line":4711},[137,36439,516],{"emptyLinePlaceholder":515},[137,36441,36442,36444,36447,36449,36451,36453,36455,36457],{"class":139,"line":4716},[137,36443,5772],{"class":143},[137,36445,36446],{"class":364}," textElement",[137,36448,151],{"class":143},[137,36450,3717],{"class":157},[137,36452,24422],{"class":147},[137,36454,356],{"class":157},[137,36456,7837],{"class":284},[137,36458,1502],{"class":157},[137,36460,36461,36463,36466,36468,36470,36472,36474,36476],{"class":139,"line":4721},[137,36462,5772],{"class":143},[137,36464,36465],{"class":364}," inputElement",[137,36467,151],{"class":143},[137,36469,3717],{"class":157},[137,36471,24422],{"class":147},[137,36473,356],{"class":157},[137,36475,36224],{"class":284},[137,36477,1502],{"class":157},[137,36479,36480],{"class":139,"line":4727},[137,36481,516],{"emptyLinePlaceholder":515},[137,36483,36484],{"class":139,"line":4732},[137,36485,36486],{"class":308},"            \u002F\u002F Create a reactive ref\n",[137,36488,36489,36491,36493,36495,36497,36499,36501,36503,36505,36507,36509],{"class":139,"line":5006},[137,36490,5772],{"class":143},[137,36492,891],{"class":364},[137,36494,151],{"class":143},[137,36496,10468],{"class":147},[137,36498,356],{"class":157},[137,36500,35661],{"class":284},[137,36502,24531],{"class":157},[137,36504,6125],{"class":161},[137,36506,219],{"class":157},[137,36508,222],{"class":143},[137,36510,256],{"class":157},[137,36512,36513,36516,36518,36521,36523,36526,36528],{"class":139,"line":5014},[137,36514,36515],{"class":157},"                textElement.textContent ",[137,36517,253],{"class":143},[137,36519,36520],{"class":284}," `Hello, ${",[137,36522,6125],{"class":157},[137,36524,36525],{"class":284},"}!`",[137,36527,2323],{"class":157},[137,36529,36530],{"class":308},"\u002F\u002F Update the DOM reactively\n",[137,36532,36533],{"class":139,"line":14343},[137,36534,14336],{"class":157},[137,36536,36537],{"class":139,"line":24199},[137,36538,516],{"emptyLinePlaceholder":515},[137,36540,36541],{"class":139,"line":24773},[137,36542,36543],{"class":308},"            \u002F\u002F Initialize the DOM\n",[137,36545,36546,36549,36551,36553,36555,36557,36559,36561],{"class":139,"line":24778},[137,36547,36548],{"class":157},"            textElement.textContent ",[137,36550,253],{"class":143},[137,36552,36520],{"class":284},[137,36554,1387],{"class":157},[137,36556,1017],{"class":284},[137,36558,5414],{"class":157},[137,36560,36525],{"class":284},[137,36562,3276],{"class":157},[137,36564,36565],{"class":139,"line":24783},[137,36566,516],{"emptyLinePlaceholder":515},[137,36568,36569],{"class":139,"line":24793},[137,36570,36571],{"class":308},"            \u002F\u002F Listen for input changes\n",[137,36573,36574,36577,36579,36581,36583,36585,36587,36589,36591],{"class":139,"line":24827},[137,36575,36576],{"class":157},"            inputElement.",[137,36578,4412],{"class":147},[137,36580,356],{"class":157},[137,36582,36224],{"class":284},[137,36584,24531],{"class":157},[137,36586,24689],{"class":161},[137,36588,219],{"class":157},[137,36590,222],{"class":143},[137,36592,256],{"class":157},[137,36594,36595,36597,36599,36601],{"class":139,"line":24857},[137,36596,34330],{"class":143},[137,36598,35527],{"class":364},[137,36600,151],{"class":143},[137,36602,36603],{"class":157}," event.target;\n",[137,36605,36606,36609,36611,36614],{"class":139,"line":24862},[137,36607,36608],{"class":157},"                name.value ",[137,36610,253],{"class":143},[137,36612,36613],{"class":157}," target.value; ",[137,36615,36616],{"class":308},"\u002F\u002F Trigger reactivity\n",[137,36618,36619],{"class":139,"line":24867},[137,36620,14336],{"class":157},[137,36622,36623,36625,36627],{"class":139,"line":24884},[137,36624,9843],{"class":157},[137,36626,4037],{"class":4036},[137,36628,4053],{"class":157},[137,36630,36631,36633,36635],{"class":139,"line":24892},[137,36632,8374],{"class":157},[137,36634,4065],{"class":4036},[137,36636,4053],{"class":157},[137,36638,36639,36641,36643],{"class":139,"line":24902},[137,36640,4083],{"class":157},[137,36642,4026],{"class":4036},[137,36644,4053],{"class":157},[123,36646,36648],{"id":36647},"how-it-works","How It Works:",[2569,36650,36651,36664,36673],{},[1006,36652,36653,36656,36657,36660,36661,36663],{},[42,36654,36655],{},"Reactive Updates:"," When ",[22,36658,36659],{},"name.value"," changes, the callback updates the DOM (",[22,36662,3964],{}," element).",[1006,36665,36666,36669,36670,36672],{},[42,36667,36668],{},"Two-Way Binding:"," Changes in the input field update the ",[22,36671,36659],{},", and the DOM reacts accordingly.",[1006,36674,36675,36678],{},[42,36676,36677],{},"Reactivity Simplified:"," This mimics Vue.js's two-way data binding system with minimal code.",[104,36680,2567],{"id":2566},[27,36682,36683,36684,36686,36687,36689,36690,36692],{},"Using JavaScript's ",[22,36685,35011],{},", we've built a simple yet powerful reactivity system. We started with basic change tracking, moved to a reusable ",[22,36688,27791],{}," function, simplified it with ",[22,36691,27815],{},", added type safety with TypeScript, and finally created a reactive HTML system.",[27,36694,36695],{},"This demonstrates how modern frameworks implement reactivity under the hood, and you can now experiment with your own lightweight reactive systems.",[27,36697,36698,36699,1017],{},"The code for this is available in the following GitHub repository ",[45,36700,10647],{"href":36701,"target":2716,"rel":36702},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Funderstanding-javascript-reactivity-with-proxy-and-typescript?tab=readme-ov-file",[2718,2719],[2617,36704,36705],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":133,"searchDepth":173,"depth":173,"links":36707},[36708,36709,36711,36713,36714,36717],{"id":35015,"depth":173,"text":35016},{"id":35227,"depth":173,"text":36710},"Step 2: Creating a Reusable watch Function",{"id":35482,"depth":173,"text":36712},"Step 3: Simplifying Reactivity with ref",{"id":35737,"depth":173,"text":35738},{"id":36075,"depth":173,"text":36076,"children":36715},[36716],{"id":36647,"depth":188,"text":36648},{"id":2566,"depth":173,"text":2567},"Learn how to create a reactive system in JavaScript using Proxy, step-by-step, from monitoring changes to implementing a Vue.js-inspired ref function. This guide covers everything from simple examples to advanced TypeScript integration, culminating in a practical demo of two-way HTML bindings. Perfect for developers exploring reactivity concepts and building modern web apps.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1735957390\u002Fblog\u002Funderstanding-javascript-reactivity-with-proxy-and-typescript\u002Fyklh8nkuomwfu8gbmcqq",[29177,35011,8,12817,36721,36722,8448,5300,5299,36723,36724,36725,36726,36727,36728,36729,36730,36731,36732,36733,36734,36735,36736,36737],"Reactivity in JavaScript","Reactivity","JavaScript Proxy","JavaScript reactivity","TypeScript reactivity","Reactive programming","JavaScript reactive systems","Vue.js reactivity","Two-way data binding","TypeScript Proxy example","JavaScript Proxy tutorial","TypeScript reactive variables","Build reactive system JavaScript","TypeScript two-way binding","Reactive programming tutorial","Proxy-based reactivity","TypeScript DOM reactivity",{},"\u002F2025\u002F01\u002F04\u002Funderstanding-javascript-reactivity-with-proxy-and-typescript","4th January 2025",{"title":34974,"description":36718},"2025\u002F01\u002F04\u002Funderstanding-javascript-reactivity-with-proxy-and-typescript","HaCQT-Ml0y1cuSaPh_BSvUDS-ch5_swA4lW-0ACPIYo",{"id":36745,"title":36746,"articleTags":36747,"author":11,"blog":12,"body":36748,"description":39209,"extension":2649,"image":39210,"keywords":39211,"meta":39227,"navigation":515,"path":39228,"published":39229,"readTime":291,"seo":39230,"stem":39231,"type":2662,"__hash__":39232},"content\u002F2025\u002F01\u002F09\u002Fthe-container-presentational-pattern-with-react-and-vue.md","The Container\u002FPresentational Pattern with React and Vue",[29177,8,9],{"type":14,"value":36749,"toc":39181},[36750,36753,36767,36769,36773,36778,36791,36794,36814,36820,36823,36855,36864,36870,36877,36882,36979,36985,36992,37197,37204,37210,37510,37514,37643,37647,37653,37658,37783,37789,37794,38019,38025,38030,38285,38288,38425,38429,38433,38436,38669,38673,38776,38780,38783,38994,38998,39129,39133,39148,39159,39172,39178],[17,36751,36746],{"id":36752},"the-containerpresentational-pattern-with-react-and-vue",[27,36754,36755],{},[30,36756,36757,36,36759,40,36761],{},[33,36758],{"value":35},[33,36760],{"value":39},[42,36762,36763],{},[45,36764,36765],{"href":47},[33,36766],{"value":50},[52,36768],{":tags":54},[56,36770],{":audio-src":36771,":transcript-src":36772},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F01\u002F09\u002Fthe-container-presentational-pattern-with-react-and-vue\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F01\u002F09\u002Fthe-container-presentational-pattern-with-react-and-vue\u002Fsummary.json",[27,36774,36775],{},[63,36776],{"alt":12847,"src":36777},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1736332534\u002Fblog\u002Fthe-container-presentational-pattern-with-react-and-vue\u002Fthe-container-presentational-pattern-with-react-and-vue_tx5c9t",[27,36779,4737,36780,36783,36784,114,36787,36790],{},[42,36781,36782],{},"Container\u002FPresentational Pattern"," is a fundamental concept in frontend development. By separating logic and UI, it creates clean, reusable, and testable components. Traditionally, this pattern was implemented using \"container components\" to manage state and \"presentational components\" to handle UI rendering. However, modern tools like ",[42,36785,36786],{},"React Hooks",[42,36788,36789],{},"Vue Composables"," provide an alternative, more modular way to achieve the same goals, without relying on explicit container components.",[27,36792,36793],{},"In this blog post, we’ll:",[2569,36795,36796,36804],{},[1006,36797,36798,36799,114,36802,1017],{},"Explore the traditional implementation of the Container\u002FPresentational Pattern in ",[42,36800,36801],{},"React",[42,36803,26585],{},[1006,36805,36806,36807,114,36810,36813],{},"Enhance the pattern using ",[42,36808,36809],{},"Hooks",[42,36811,36812],{},"Composables"," for a cleaner, reusable design approach.",[104,36815,36817],{"id":36816},"introduction-to-the-containerpresentational-pattern",[42,36818,36819],{},"Introduction to the Container\u002FPresentational Pattern",[27,36821,36822],{},"The Container\u002FPresentational Pattern divides components into two types:",[2569,36824,36825,36842],{},[1006,36826,36827,36830,36831],{},[42,36828,36829],{},"Presentational Components",":\n",[1003,36832,36833,36836,36839],{},[1006,36834,36835],{},"Focus on rendering the UI.",[1006,36837,36838],{},"Receive data via props.",[1006,36840,36841],{},"Are stateless and reusable across different contexts.",[1006,36843,36844,36830,36847],{},[42,36845,36846],{},"Container Components",[1003,36848,36849,36852],{},[1006,36850,36851],{},"Manage state, handle logic, and perform data fetching.",[1006,36853,36854],{},"Pass data to presentational components.",[27,36856,36857,36858,114,36860,36863],{},"This separation of concerns promotes maintainability and reusability. However, in modern frameworks like React and Vue, container components can be replaced by ",[42,36859,36809],{},[42,36861,36862],{},"Composable functions",", encapsulating logic in reusable, framework-specific utilities.",[104,36865,36867],{"id":36866},"react-implementation-with-traditional-container-components",[42,36868,36869],{},"React Implementation with Traditional Container Components",[123,36871,36873,36874],{"id":36872},"presentational-component-cardjsx","Presentational Component - ",[22,36875,36876],{},"Card.jsx",[27,36878,4737,36879,36881],{},[22,36880,30584],{}," component in React is focused purely on displaying user information:",[128,36883,36886],{"className":36884,"code":36885,"language":29196,"meta":133,"style":133},"language-jsx shiki shiki-themes github-light github-dark","function Card({ data }) {\n    return (\n        \u003Cdiv className=\"card\">\n            \u003Ch2>{data.name}\u003C\u002Fh2>\n            \u003Cp>Age: {data.age}\u003C\u002Fp>\n        \u003C\u002Fdiv>\n    );\n}\n\nexport default Card;\n",[22,36887,36888,36902,36908,36924,36937,36950,36958,36962,36966,36970],{"__ignoreMap":133},[137,36889,36890,36892,36894,36897,36899],{"class":139,"line":140},[137,36891,483],{"class":143},[137,36893,30397],{"class":147},[137,36895,36896],{"class":157},"({ ",[137,36898,1942],{"class":161},[137,36900,36901],{"class":157}," }) {\n",[137,36903,36904,36906],{"class":139,"line":173},[137,36905,176],{"class":143},[137,36907,30009],{"class":157},[137,36909,36910,36912,36914,36917,36919,36922],{"class":139,"line":188},[137,36911,9826],{"class":157},[137,36913,8330],{"class":4036},[137,36915,36916],{"class":147}," className",[137,36918,253],{"class":143},[137,36920,36921],{"class":284},"\"card\"",[137,36923,4053],{"class":157},[137,36925,36926,36928,36930,36933,36935],{"class":139,"line":269},[137,36927,23852],{"class":157},[137,36929,104],{"class":4036},[137,36931,36932],{"class":157},">{data.name}\u003C\u002F",[137,36934,104],{"class":4036},[137,36936,4053],{"class":157},[137,36938,36939,36941,36943,36946,36948],{"class":139,"line":278},[137,36940,23852],{"class":157},[137,36942,27],{"class":4036},[137,36944,36945],{"class":157},">Age: {data.age}\u003C\u002F",[137,36947,27],{"class":4036},[137,36949,4053],{"class":157},[137,36951,36952,36954,36956],{"class":139,"line":291},[137,36953,9843],{"class":157},[137,36955,8330],{"class":4036},[137,36957,4053],{"class":157},[137,36959,36960],{"class":139,"line":297},[137,36961,11875],{"class":157},[137,36963,36964],{"class":139,"line":302},[137,36965,510],{"class":157},[137,36967,36968],{"class":139,"line":662},[137,36969,516],{"emptyLinePlaceholder":515},[137,36971,36972,36974,36976],{"class":139,"line":667},[137,36973,13456],{"class":143},[137,36975,21723],{"class":143},[137,36977,36978],{"class":157}," Card;\n",[123,36980,36873,36982],{"id":36981},"presentational-component-detailsjsx",[22,36983,36984],{},"Details.jsx",[27,36986,36987,36988,36991],{},"Similarly, the ",[22,36989,36990],{},"Details"," component handles additional user details:",[128,36993,36995],{"className":36884,"code":36994,"language":29196,"meta":133,"style":133},"function Card({ data }) {\n    return (\n        \u003Cdiv className=\"card\">\n            \u003Cp>Email: {data.email}\u003C\u002Fp>\n            \u003Cp>Address: {data.address}\u003C\u002Fp>\n            \u003Cp>City: {data.city}\u003C\u002Fp>\n            \u003Cp>State: {data.state}\u003C\u002Fp>\n            \u003Cp>Zip: {data.zip}\u003C\u002Fp>\n            \u003Cp>Phone: {data.phone}\u003C\u002Fp>\n            \u003Cp>Occupation: {data.occupation}\u003C\u002Fp>\n            \u003Cp>Company: {data.company}\u003C\u002Fp>\n            \u003Cp>Hobbies: {data.hobbies?.join(\", \")}\u003C\u002Fp>\n            \u003Cp>Website: {data.website}\u003C\u002Fp>\n        \u003C\u002Fdiv>\n    );\n}\n\nexport default Card;\n",[22,36996,36997,37009,37015,37029,37042,37055,37068,37081,37094,37107,37120,37133,37156,37169,37177,37181,37185,37189],{"__ignoreMap":133},[137,36998,36999,37001,37003,37005,37007],{"class":139,"line":140},[137,37000,483],{"class":143},[137,37002,30397],{"class":147},[137,37004,36896],{"class":157},[137,37006,1942],{"class":161},[137,37008,36901],{"class":157},[137,37010,37011,37013],{"class":139,"line":173},[137,37012,176],{"class":143},[137,37014,30009],{"class":157},[137,37016,37017,37019,37021,37023,37025,37027],{"class":139,"line":188},[137,37018,9826],{"class":157},[137,37020,8330],{"class":4036},[137,37022,36916],{"class":147},[137,37024,253],{"class":143},[137,37026,36921],{"class":284},[137,37028,4053],{"class":157},[137,37030,37031,37033,37035,37038,37040],{"class":139,"line":269},[137,37032,23852],{"class":157},[137,37034,27],{"class":4036},[137,37036,37037],{"class":157},">Email: {data.email}\u003C\u002F",[137,37039,27],{"class":4036},[137,37041,4053],{"class":157},[137,37043,37044,37046,37048,37051,37053],{"class":139,"line":278},[137,37045,23852],{"class":157},[137,37047,27],{"class":4036},[137,37049,37050],{"class":157},">Address: {data.address}\u003C\u002F",[137,37052,27],{"class":4036},[137,37054,4053],{"class":157},[137,37056,37057,37059,37061,37064,37066],{"class":139,"line":291},[137,37058,23852],{"class":157},[137,37060,27],{"class":4036},[137,37062,37063],{"class":157},">City: {data.city}\u003C\u002F",[137,37065,27],{"class":4036},[137,37067,4053],{"class":157},[137,37069,37070,37072,37074,37077,37079],{"class":139,"line":297},[137,37071,23852],{"class":157},[137,37073,27],{"class":4036},[137,37075,37076],{"class":157},">State: {data.state}\u003C\u002F",[137,37078,27],{"class":4036},[137,37080,4053],{"class":157},[137,37082,37083,37085,37087,37090,37092],{"class":139,"line":302},[137,37084,23852],{"class":157},[137,37086,27],{"class":4036},[137,37088,37089],{"class":157},">Zip: {data.zip}\u003C\u002F",[137,37091,27],{"class":4036},[137,37093,4053],{"class":157},[137,37095,37096,37098,37100,37103,37105],{"class":139,"line":662},[137,37097,23852],{"class":157},[137,37099,27],{"class":4036},[137,37101,37102],{"class":157},">Phone: {data.phone}\u003C\u002F",[137,37104,27],{"class":4036},[137,37106,4053],{"class":157},[137,37108,37109,37111,37113,37116,37118],{"class":139,"line":667},[137,37110,23852],{"class":157},[137,37112,27],{"class":4036},[137,37114,37115],{"class":157},">Occupation: {data.occupation}\u003C\u002F",[137,37117,27],{"class":4036},[137,37119,4053],{"class":157},[137,37121,37122,37124,37126,37129,37131],{"class":139,"line":786},[137,37123,23852],{"class":157},[137,37125,27],{"class":4036},[137,37127,37128],{"class":157},">Company: {data.company}\u003C\u002F",[137,37130,27],{"class":4036},[137,37132,4053],{"class":157},[137,37134,37135,37137,37139,37142,37144,37146,37149,37152,37154],{"class":139,"line":798},[137,37136,23852],{"class":157},[137,37138,27],{"class":4036},[137,37140,37141],{"class":157},">Hobbies: {data.hobbies?.",[137,37143,8628],{"class":147},[137,37145,356],{"class":157},[137,37147,37148],{"class":284},"\", \"",[137,37150,37151],{"class":157},")}\u003C\u002F",[137,37153,27],{"class":4036},[137,37155,4053],{"class":157},[137,37157,37158,37160,37162,37165,37167],{"class":139,"line":803},[137,37159,23852],{"class":157},[137,37161,27],{"class":4036},[137,37163,37164],{"class":157},">Website: {data.website}\u003C\u002F",[137,37166,27],{"class":4036},[137,37168,4053],{"class":157},[137,37170,37171,37173,37175],{"class":139,"line":931},[137,37172,9843],{"class":157},[137,37174,8330],{"class":4036},[137,37176,4053],{"class":157},[137,37178,37179],{"class":139,"line":1568},[137,37180,11875],{"class":157},[137,37182,37183],{"class":139,"line":1573},[137,37184,510],{"class":157},[137,37186,37187],{"class":139,"line":1578},[137,37188,516],{"emptyLinePlaceholder":515},[137,37190,37191,37193,37195],{"class":139,"line":1588},[137,37192,13456],{"class":143},[137,37194,21723],{"class":143},[137,37196,36978],{"class":157},[123,37198,37200,37201],{"id":37199},"container-component-datacontainerjsx","Container Component - ",[22,37202,37203],{},"DataContainer.jsx",[27,37205,4737,37206,37209],{},[22,37207,37208],{},"DataContainer"," component manages data fetching and distributes the fetched data to child components.",[128,37211,37213],{"className":36884,"code":37212,"language":29196,"meta":133,"style":133},"import { useState, useEffect, Children, cloneElement } from \"react\";\n\nfunction DataContainer({ children }) {\n    const [data, setData] = useState(null);\n\n    useEffect(() => {\n        \u002F\u002F Simulating data fetching\n        setTimeout(() => {\n            setData({\n                name: \"John Doe\",\n                age: 30,\n                email: \"john.doe@example.com\",\n                address: \"123 Main St\",\n                city: \"Anytown\",\n                state: \"CA\",\n                zip: \"12345\",\n                phone: \"555-1234\",\n                occupation: \"Software Developer\",\n                company: \"Tech Corp\",\n                hobbies: [\"reading\", \"gaming\", \"hiking\"],\n                website: \"https:\u002F\u002Fjohndoe.com\",\n            });\n        }, 1000);\n    }, []);\n\n    return \u003C>{children && Children.map(children, (child) => cloneElement(child, { data }))}\u003C\u002F>;\n}\n\nexport default DataContainer;\n",[22,37214,37215,37229,37233,37247,37273,37277,37288,37293,37303,37310,37320,37330,37340,37350,37360,37370,37380,37390,37400,37410,37430,37440,37444,37453,37458,37462,37493,37497,37501],{"__ignoreMap":133},[137,37216,37217,37219,37222,37224,37227],{"class":139,"line":140},[137,37218,10287],{"class":143},[137,37220,37221],{"class":157}," { useState, useEffect, Children, cloneElement } ",[137,37223,10954],{"class":143},[137,37225,37226],{"class":284}," \"react\"",[137,37228,3276],{"class":157},[137,37230,37231],{"class":139,"line":173},[137,37232,516],{"emptyLinePlaceholder":515},[137,37234,37235,37237,37240,37242,37245],{"class":139,"line":188},[137,37236,483],{"class":143},[137,37238,37239],{"class":147}," DataContainer",[137,37241,36896],{"class":157},[137,37243,37244],{"class":161},"children",[137,37246,36901],{"class":157},[137,37248,37249,37251,37253,37255,37257,37260,37262,37264,37267,37269,37271],{"class":139,"line":269},[137,37250,4177],{"class":143},[137,37252,22130],{"class":157},[137,37254,1942],{"class":364},[137,37256,164],{"class":157},[137,37258,37259],{"class":364},"setData",[137,37261,5796],{"class":157},[137,37263,253],{"class":143},[137,37265,37266],{"class":147}," useState",[137,37268,356],{"class":157},[137,37270,11700],{"class":364},[137,37272,1502],{"class":157},[137,37274,37275],{"class":139,"line":278},[137,37276,516],{"emptyLinePlaceholder":515},[137,37278,37279,37282,37284,37286],{"class":139,"line":291},[137,37280,37281],{"class":147},"    useEffect",[137,37283,3193],{"class":157},[137,37285,222],{"class":143},[137,37287,256],{"class":157},[137,37289,37290],{"class":139,"line":297},[137,37291,37292],{"class":308},"        \u002F\u002F Simulating data fetching\n",[137,37294,37295,37297,37299,37301],{"class":139,"line":302},[137,37296,34665],{"class":147},[137,37298,3193],{"class":157},[137,37300,222],{"class":143},[137,37302,256],{"class":157},[137,37304,37305,37308],{"class":139,"line":662},[137,37306,37307],{"class":147},"            setData",[137,37309,3175],{"class":157},[137,37311,37312,37315,37318],{"class":139,"line":667},[137,37313,37314],{"class":157},"                name: ",[137,37316,37317],{"class":284},"\"John Doe\"",[137,37319,1961],{"class":157},[137,37321,37322,37325,37328],{"class":139,"line":786},[137,37323,37324],{"class":157},"                age: ",[137,37326,37327],{"class":364},"30",[137,37329,1961],{"class":157},[137,37331,37332,37335,37338],{"class":139,"line":798},[137,37333,37334],{"class":157},"                email: ",[137,37336,37337],{"class":284},"\"john.doe@example.com\"",[137,37339,1961],{"class":157},[137,37341,37342,37345,37348],{"class":139,"line":803},[137,37343,37344],{"class":157},"                address: ",[137,37346,37347],{"class":284},"\"123 Main St\"",[137,37349,1961],{"class":157},[137,37351,37352,37355,37358],{"class":139,"line":931},[137,37353,37354],{"class":157},"                city: ",[137,37356,37357],{"class":284},"\"Anytown\"",[137,37359,1961],{"class":157},[137,37361,37362,37365,37368],{"class":139,"line":1568},[137,37363,37364],{"class":157},"                state: ",[137,37366,37367],{"class":284},"\"CA\"",[137,37369,1961],{"class":157},[137,37371,37372,37375,37378],{"class":139,"line":1573},[137,37373,37374],{"class":157},"                zip: ",[137,37376,37377],{"class":284},"\"12345\"",[137,37379,1961],{"class":157},[137,37381,37382,37385,37388],{"class":139,"line":1578},[137,37383,37384],{"class":157},"                phone: ",[137,37386,37387],{"class":284},"\"555-1234\"",[137,37389,1961],{"class":157},[137,37391,37392,37395,37398],{"class":139,"line":1588},[137,37393,37394],{"class":157},"                occupation: ",[137,37396,37397],{"class":284},"\"Software Developer\"",[137,37399,1961],{"class":157},[137,37401,37402,37405,37408],{"class":139,"line":1601},[137,37403,37404],{"class":157},"                company: ",[137,37406,37407],{"class":284},"\"Tech Corp\"",[137,37409,1961],{"class":157},[137,37411,37412,37415,37418,37420,37423,37425,37428],{"class":139,"line":3802},[137,37413,37414],{"class":157},"                hobbies: [",[137,37416,37417],{"class":284},"\"reading\"",[137,37419,164],{"class":157},[137,37421,37422],{"class":284},"\"gaming\"",[137,37424,164],{"class":157},[137,37426,37427],{"class":284},"\"hiking\"",[137,37429,21916],{"class":157},[137,37431,37432,37435,37438],{"class":139,"line":3808},[137,37433,37434],{"class":157},"                website: ",[137,37436,37437],{"class":284},"\"https:\u002F\u002Fjohndoe.com\"",[137,37439,1961],{"class":157},[137,37441,37442],{"class":139,"line":3822},[137,37443,14336],{"class":157},[137,37445,37446,37448,37451],{"class":139,"line":3827},[137,37447,34717],{"class":157},[137,37449,37450],{"class":364},"1000",[137,37452,1502],{"class":157},[137,37454,37455],{"class":139,"line":3832},[137,37456,37457],{"class":157},"    }, []);\n",[137,37459,37460],{"class":139,"line":3840},[137,37461,516],{"emptyLinePlaceholder":515},[137,37463,37464,37466,37469,37471,37474,37477,37480,37483,37485,37487,37490],{"class":139,"line":3846},[137,37465,176],{"class":143},[137,37467,37468],{"class":157}," \u003C>{children ",[137,37470,3351],{"class":143},[137,37472,37473],{"class":157}," Children.",[137,37475,37476],{"class":147},"map",[137,37478,37479],{"class":157},"(children, (",[137,37481,37482],{"class":161},"child",[137,37484,219],{"class":157},[137,37486,222],{"class":143},[137,37488,37489],{"class":147}," cloneElement",[137,37491,37492],{"class":157},"(child, { data }))}\u003C\u002F>;\n",[137,37494,37495],{"class":139,"line":3861},[137,37496,510],{"class":157},[137,37498,37499],{"class":139,"line":3883},[137,37500,516],{"emptyLinePlaceholder":515},[137,37502,37503,37505,37507],{"class":139,"line":3896},[137,37504,13456],{"class":143},[137,37506,21723],{"class":143},[137,37508,37509],{"class":157}," DataContainer;\n",[123,37511,37513],{"id":37512},"using-the-components-together","Using the Components Together",[128,37515,37517],{"className":36884,"code":37516,"language":29196,"meta":133,"style":133},"import Card from \".\u002Fcomponents\u002FCard\";\nimport Details from \".\u002Fcomponents\u002FDetails\";\nimport DataContainer from \".\u002Fcomponents\u002FDataContainer\";\n\nfunction App() {\n    return (\n        \u003C>\n            \u003CDataContainer>\n                \u003CCard \u002F>\n                \u003CDetails \u002F>\n            \u003C\u002FDataContainer>\n        \u003C\u002F>\n    );\n}\n\nexport default App;\n",[22,37518,37519,37533,37547,37561,37565,37574,37580,37585,37593,37601,37609,37617,37622,37626,37630,37634],{"__ignoreMap":133},[137,37520,37521,37523,37526,37528,37531],{"class":139,"line":140},[137,37522,10287],{"class":143},[137,37524,37525],{"class":157}," Card ",[137,37527,10954],{"class":143},[137,37529,37530],{"class":284}," \".\u002Fcomponents\u002FCard\"",[137,37532,3276],{"class":157},[137,37534,37535,37537,37540,37542,37545],{"class":139,"line":173},[137,37536,10287],{"class":143},[137,37538,37539],{"class":157}," Details ",[137,37541,10954],{"class":143},[137,37543,37544],{"class":284}," \".\u002Fcomponents\u002FDetails\"",[137,37546,3276],{"class":157},[137,37548,37549,37551,37554,37556,37559],{"class":139,"line":188},[137,37550,10287],{"class":143},[137,37552,37553],{"class":157}," DataContainer ",[137,37555,10954],{"class":143},[137,37557,37558],{"class":284}," \".\u002Fcomponents\u002FDataContainer\"",[137,37560,3276],{"class":157},[137,37562,37563],{"class":139,"line":269},[137,37564,516],{"emptyLinePlaceholder":515},[137,37566,37567,37569,37572],{"class":139,"line":278},[137,37568,483],{"class":143},[137,37570,37571],{"class":147}," App",[137,37573,275],{"class":157},[137,37575,37576,37578],{"class":139,"line":291},[137,37577,176],{"class":143},[137,37579,30009],{"class":157},[137,37581,37582],{"class":139,"line":297},[137,37583,37584],{"class":157},"        \u003C>\n",[137,37586,37587,37589,37591],{"class":139,"line":302},[137,37588,23852],{"class":157},[137,37590,37208],{"class":364},[137,37592,4053],{"class":157},[137,37594,37595,37597,37599],{"class":139,"line":662},[137,37596,23861],{"class":157},[137,37598,30584],{"class":364},[137,37600,4078],{"class":157},[137,37602,37603,37605,37607],{"class":139,"line":667},[137,37604,23861],{"class":157},[137,37606,36990],{"class":364},[137,37608,4078],{"class":157},[137,37610,37611,37613,37615],{"class":139,"line":786},[137,37612,23980],{"class":157},[137,37614,37208],{"class":364},[137,37616,4053],{"class":157},[137,37618,37619],{"class":139,"line":798},[137,37620,37621],{"class":157},"        \u003C\u002F>\n",[137,37623,37624],{"class":139,"line":803},[137,37625,11875],{"class":157},[137,37627,37628],{"class":139,"line":931},[137,37629,510],{"class":157},[137,37631,37632],{"class":139,"line":1568},[137,37633,516],{"emptyLinePlaceholder":515},[137,37635,37636,37638,37640],{"class":139,"line":1573},[137,37637,13456],{"class":143},[137,37639,21723],{"class":143},[137,37641,37642],{"class":157}," App;\n",[104,37644,37646],{"id":37645},"vue-3-implementation-with-traditional-container-components","Vue 3 Implementation with Traditional Container Components",[123,37648,36873,37650],{"id":37649},"presentational-component-cardvue",[22,37651,37652],{},"Card.vue",[27,37654,4737,37655,37657],{},[22,37656,30584],{}," component in Vue focuses on rendering basic user information:",[128,37659,37661],{"className":4024,"code":37660,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import { defineProps } from \"vue\";\n\n    defineProps({\n        data: Object,\n    });\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv class=\"card\">\n        \u003Ch2>{{ data.name }}\u003C\u002Fh2>\n        \u003Cp>Age: {{ data.age }}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,37662,37663,37673,37687,37691,37698,37703,37707,37715,37719,37727,37741,37754,37767,37775],{"__ignoreMap":133},[137,37664,37665,37667,37669,37671],{"class":139,"line":140},[137,37666,4033],{"class":157},[137,37668,4037],{"class":4036},[137,37670,9642],{"class":147},[137,37672,4053],{"class":157},[137,37674,37675,37678,37681,37683,37685],{"class":139,"line":173},[137,37676,37677],{"class":143},"    import",[137,37679,37680],{"class":157}," { defineProps } ",[137,37682,10954],{"class":143},[137,37684,11091],{"class":284},[137,37686,3276],{"class":157},[137,37688,37689],{"class":139,"line":188},[137,37690,516],{"emptyLinePlaceholder":515},[137,37692,37693,37696],{"class":139,"line":269},[137,37694,37695],{"class":147},"    defineProps",[137,37697,3175],{"class":157},[137,37699,37700],{"class":139,"line":278},[137,37701,37702],{"class":157},"        data: Object,\n",[137,37704,37705],{"class":139,"line":291},[137,37706,2832],{"class":157},[137,37708,37709,37711,37713],{"class":139,"line":297},[137,37710,4083],{"class":157},[137,37712,4037],{"class":4036},[137,37714,4053],{"class":157},[137,37716,37717],{"class":139,"line":302},[137,37718,516],{"emptyLinePlaceholder":515},[137,37720,37721,37723,37725],{"class":139,"line":662},[137,37722,4033],{"class":157},[137,37724,7821],{"class":4036},[137,37726,4053],{"class":157},[137,37728,37729,37731,37733,37735,37737,37739],{"class":139,"line":667},[137,37730,4072],{"class":157},[137,37732,8330],{"class":4036},[137,37734,7832],{"class":147},[137,37736,253],{"class":157},[137,37738,36921],{"class":284},[137,37740,4053],{"class":157},[137,37742,37743,37745,37747,37750,37752],{"class":139,"line":786},[137,37744,9826],{"class":157},[137,37746,104],{"class":4036},[137,37748,37749],{"class":157},">{{ data.name }}\u003C\u002F",[137,37751,104],{"class":4036},[137,37753,4053],{"class":157},[137,37755,37756,37758,37760,37763,37765],{"class":139,"line":798},[137,37757,9826],{"class":157},[137,37759,27],{"class":4036},[137,37761,37762],{"class":157},">Age: {{ data.age }}\u003C\u002F",[137,37764,27],{"class":4036},[137,37766,4053],{"class":157},[137,37768,37769,37771,37773],{"class":139,"line":803},[137,37770,8374],{"class":157},[137,37772,8330],{"class":4036},[137,37774,4053],{"class":157},[137,37776,37777,37779,37781],{"class":139,"line":931},[137,37778,4083],{"class":157},[137,37780,7821],{"class":4036},[137,37782,4053],{"class":157},[123,37784,36873,37786],{"id":37785},"presentational-component-detailsvue",[22,37787,37788],{},"Details.vue",[27,37790,4737,37791,37793],{},[22,37792,36990],{}," component renders additional user details:",[128,37795,37797],{"className":4024,"code":37796,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import { defineProps } from \"vue\";\n\n    defineProps({\n        data: Object,\n    });\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv class=\"card\">\n        \u003Cp>Email: {{ data.email }}\u003C\u002Fp>\n        \u003Cp>Address: {{ data.address }}\u003C\u002Fp>\n        \u003Cp>City: {{ data.city }}\u003C\u002Fp>\n        \u003Cp>State: {{ data.state }}\u003C\u002Fp>\n        \u003Cp>Zip: {{ data.zip }}\u003C\u002Fp>\n        \u003Cp>Phone: {{ data.phone }}\u003C\u002Fp>\n        \u003Cp>Occupation: {{ data.occupation }}\u003C\u002Fp>\n        \u003Cp>Company: {{ data.company }}\u003C\u002Fp>\n        \u003Cp>Hobbies: {{ data.hobbies?.join(\", \") }}\u003C\u002Fp>\n        \u003Cp>Website: {{ data.website }}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,37798,37799,37809,37821,37825,37831,37835,37839,37847,37851,37859,37873,37886,37899,37912,37925,37938,37951,37964,37977,37990,38003,38011],{"__ignoreMap":133},[137,37800,37801,37803,37805,37807],{"class":139,"line":140},[137,37802,4033],{"class":157},[137,37804,4037],{"class":4036},[137,37806,9642],{"class":147},[137,37808,4053],{"class":157},[137,37810,37811,37813,37815,37817,37819],{"class":139,"line":173},[137,37812,37677],{"class":143},[137,37814,37680],{"class":157},[137,37816,10954],{"class":143},[137,37818,11091],{"class":284},[137,37820,3276],{"class":157},[137,37822,37823],{"class":139,"line":188},[137,37824,516],{"emptyLinePlaceholder":515},[137,37826,37827,37829],{"class":139,"line":269},[137,37828,37695],{"class":147},[137,37830,3175],{"class":157},[137,37832,37833],{"class":139,"line":278},[137,37834,37702],{"class":157},[137,37836,37837],{"class":139,"line":291},[137,37838,2832],{"class":157},[137,37840,37841,37843,37845],{"class":139,"line":297},[137,37842,4083],{"class":157},[137,37844,4037],{"class":4036},[137,37846,4053],{"class":157},[137,37848,37849],{"class":139,"line":302},[137,37850,516],{"emptyLinePlaceholder":515},[137,37852,37853,37855,37857],{"class":139,"line":662},[137,37854,4033],{"class":157},[137,37856,7821],{"class":4036},[137,37858,4053],{"class":157},[137,37860,37861,37863,37865,37867,37869,37871],{"class":139,"line":667},[137,37862,4072],{"class":157},[137,37864,8330],{"class":4036},[137,37866,7832],{"class":147},[137,37868,253],{"class":157},[137,37870,36921],{"class":284},[137,37872,4053],{"class":157},[137,37874,37875,37877,37879,37882,37884],{"class":139,"line":786},[137,37876,9826],{"class":157},[137,37878,27],{"class":4036},[137,37880,37881],{"class":157},">Email: {{ data.email }}\u003C\u002F",[137,37883,27],{"class":4036},[137,37885,4053],{"class":157},[137,37887,37888,37890,37892,37895,37897],{"class":139,"line":798},[137,37889,9826],{"class":157},[137,37891,27],{"class":4036},[137,37893,37894],{"class":157},">Address: {{ data.address }}\u003C\u002F",[137,37896,27],{"class":4036},[137,37898,4053],{"class":157},[137,37900,37901,37903,37905,37908,37910],{"class":139,"line":803},[137,37902,9826],{"class":157},[137,37904,27],{"class":4036},[137,37906,37907],{"class":157},">City: {{ data.city }}\u003C\u002F",[137,37909,27],{"class":4036},[137,37911,4053],{"class":157},[137,37913,37914,37916,37918,37921,37923],{"class":139,"line":931},[137,37915,9826],{"class":157},[137,37917,27],{"class":4036},[137,37919,37920],{"class":157},">State: {{ data.state }}\u003C\u002F",[137,37922,27],{"class":4036},[137,37924,4053],{"class":157},[137,37926,37927,37929,37931,37934,37936],{"class":139,"line":1568},[137,37928,9826],{"class":157},[137,37930,27],{"class":4036},[137,37932,37933],{"class":157},">Zip: {{ data.zip }}\u003C\u002F",[137,37935,27],{"class":4036},[137,37937,4053],{"class":157},[137,37939,37940,37942,37944,37947,37949],{"class":139,"line":1573},[137,37941,9826],{"class":157},[137,37943,27],{"class":4036},[137,37945,37946],{"class":157},">Phone: {{ data.phone }}\u003C\u002F",[137,37948,27],{"class":4036},[137,37950,4053],{"class":157},[137,37952,37953,37955,37957,37960,37962],{"class":139,"line":1578},[137,37954,9826],{"class":157},[137,37956,27],{"class":4036},[137,37958,37959],{"class":157},">Occupation: {{ data.occupation }}\u003C\u002F",[137,37961,27],{"class":4036},[137,37963,4053],{"class":157},[137,37965,37966,37968,37970,37973,37975],{"class":139,"line":1588},[137,37967,9826],{"class":157},[137,37969,27],{"class":4036},[137,37971,37972],{"class":157},">Company: {{ data.company }}\u003C\u002F",[137,37974,27],{"class":4036},[137,37976,4053],{"class":157},[137,37978,37979,37981,37983,37986,37988],{"class":139,"line":1601},[137,37980,9826],{"class":157},[137,37982,27],{"class":4036},[137,37984,37985],{"class":157},">Hobbies: {{ data.hobbies?.join(\", \") }}\u003C\u002F",[137,37987,27],{"class":4036},[137,37989,4053],{"class":157},[137,37991,37992,37994,37996,37999,38001],{"class":139,"line":3802},[137,37993,9826],{"class":157},[137,37995,27],{"class":4036},[137,37997,37998],{"class":157},">Website: {{ data.website }}\u003C\u002F",[137,38000,27],{"class":4036},[137,38002,4053],{"class":157},[137,38004,38005,38007,38009],{"class":139,"line":3808},[137,38006,8374],{"class":157},[137,38008,8330],{"class":4036},[137,38010,4053],{"class":157},[137,38012,38013,38015,38017],{"class":139,"line":3822},[137,38014,4083],{"class":157},[137,38016,7821],{"class":4036},[137,38018,4053],{"class":157},[123,38020,37200,38022],{"id":38021},"container-component-datacontainervue",[22,38023,38024],{},"DataContainer.vue",[27,38026,4737,38027,38029],{},[22,38028,37208],{}," encapsulates data fetching and passes it to its slot:",[128,38031,38033],{"className":4024,"code":38032,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import { ref, onMounted } from \"vue\";\n\n    const data = ref(null);\n\n    onMounted(() => {\n        \u002F\u002F Simulating data fetching\n        setTimeout(() => {\n            data.value = {\n                name: \"John Doe\",\n                age: 30,\n                email: \"john.doe@example.com\",\n                address: \"123 Main St\",\n                city: \"Anytown\",\n                state: \"CA\",\n                zip: \"12345\",\n                phone: \"555-1234\",\n                occupation: \"Software Developer\",\n                company: \"Tech Corp\",\n                hobbies: [\"reading\", \"gaming\", \"hiking\"],\n                website: \"https:\u002F\u002Fjohndoe.com\",\n            };\n        }, 1000);\n    });\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cslot :data=\"data\">\u003C\u002Fslot>\n\u003C\u002Ftemplate>\n",[22,38034,38035,38045,38057,38061,38078,38082,38092,38096,38106,38115,38123,38131,38139,38147,38155,38163,38171,38179,38187,38195,38211,38219,38224,38232,38236,38244,38248,38256,38277],{"__ignoreMap":133},[137,38036,38037,38039,38041,38043],{"class":139,"line":140},[137,38038,4033],{"class":157},[137,38040,4037],{"class":4036},[137,38042,9642],{"class":147},[137,38044,4053],{"class":157},[137,38046,38047,38049,38051,38053,38055],{"class":139,"line":173},[137,38048,37677],{"class":143},[137,38050,11662],{"class":157},[137,38052,10954],{"class":143},[137,38054,11091],{"class":284},[137,38056,3276],{"class":157},[137,38058,38059],{"class":139,"line":188},[137,38060,516],{"emptyLinePlaceholder":515},[137,38062,38063,38065,38068,38070,38072,38074,38076],{"class":139,"line":269},[137,38064,4177],{"class":143},[137,38066,38067],{"class":364}," data",[137,38069,151],{"class":143},[137,38071,10468],{"class":147},[137,38073,356],{"class":157},[137,38075,11700],{"class":364},[137,38077,1502],{"class":157},[137,38079,38080],{"class":139,"line":278},[137,38081,516],{"emptyLinePlaceholder":515},[137,38083,38084,38086,38088,38090],{"class":139,"line":291},[137,38085,34654],{"class":147},[137,38087,3193],{"class":157},[137,38089,222],{"class":143},[137,38091,256],{"class":157},[137,38093,38094],{"class":139,"line":297},[137,38095,37292],{"class":308},[137,38097,38098,38100,38102,38104],{"class":139,"line":302},[137,38099,34665],{"class":147},[137,38101,3193],{"class":157},[137,38103,222],{"class":143},[137,38105,256],{"class":157},[137,38107,38108,38111,38113],{"class":139,"line":662},[137,38109,38110],{"class":157},"            data.value ",[137,38112,253],{"class":143},[137,38114,256],{"class":157},[137,38116,38117,38119,38121],{"class":139,"line":667},[137,38118,37314],{"class":157},[137,38120,37317],{"class":284},[137,38122,1961],{"class":157},[137,38124,38125,38127,38129],{"class":139,"line":786},[137,38126,37324],{"class":157},[137,38128,37327],{"class":364},[137,38130,1961],{"class":157},[137,38132,38133,38135,38137],{"class":139,"line":798},[137,38134,37334],{"class":157},[137,38136,37337],{"class":284},[137,38138,1961],{"class":157},[137,38140,38141,38143,38145],{"class":139,"line":803},[137,38142,37344],{"class":157},[137,38144,37347],{"class":284},[137,38146,1961],{"class":157},[137,38148,38149,38151,38153],{"class":139,"line":931},[137,38150,37354],{"class":157},[137,38152,37357],{"class":284},[137,38154,1961],{"class":157},[137,38156,38157,38159,38161],{"class":139,"line":1568},[137,38158,37364],{"class":157},[137,38160,37367],{"class":284},[137,38162,1961],{"class":157},[137,38164,38165,38167,38169],{"class":139,"line":1573},[137,38166,37374],{"class":157},[137,38168,37377],{"class":284},[137,38170,1961],{"class":157},[137,38172,38173,38175,38177],{"class":139,"line":1578},[137,38174,37384],{"class":157},[137,38176,37387],{"class":284},[137,38178,1961],{"class":157},[137,38180,38181,38183,38185],{"class":139,"line":1588},[137,38182,37394],{"class":157},[137,38184,37397],{"class":284},[137,38186,1961],{"class":157},[137,38188,38189,38191,38193],{"class":139,"line":1601},[137,38190,37404],{"class":157},[137,38192,37407],{"class":284},[137,38194,1961],{"class":157},[137,38196,38197,38199,38201,38203,38205,38207,38209],{"class":139,"line":3802},[137,38198,37414],{"class":157},[137,38200,37417],{"class":284},[137,38202,164],{"class":157},[137,38204,37422],{"class":284},[137,38206,164],{"class":157},[137,38208,37427],{"class":284},[137,38210,21916],{"class":157},[137,38212,38213,38215,38217],{"class":139,"line":3808},[137,38214,37434],{"class":157},[137,38216,37437],{"class":284},[137,38218,1961],{"class":157},[137,38220,38221],{"class":139,"line":3822},[137,38222,38223],{"class":157},"            };\n",[137,38225,38226,38228,38230],{"class":139,"line":3827},[137,38227,34717],{"class":157},[137,38229,37450],{"class":364},[137,38231,1502],{"class":157},[137,38233,38234],{"class":139,"line":3832},[137,38235,2832],{"class":157},[137,38237,38238,38240,38242],{"class":139,"line":3840},[137,38239,4083],{"class":157},[137,38241,4037],{"class":4036},[137,38243,4053],{"class":157},[137,38245,38246],{"class":139,"line":3846},[137,38247,516],{"emptyLinePlaceholder":515},[137,38249,38250,38252,38254],{"class":139,"line":3861},[137,38251,4033],{"class":157},[137,38253,7821],{"class":4036},[137,38255,4053],{"class":157},[137,38257,38258,38260,38263,38266,38268,38271,38273,38275],{"class":139,"line":3883},[137,38259,4072],{"class":157},[137,38261,38262],{"class":4036},"slot",[137,38264,38265],{"class":147}," :data",[137,38267,253],{"class":157},[137,38269,38270],{"class":284},"\"data\"",[137,38272,4048],{"class":157},[137,38274,38262],{"class":4036},[137,38276,4053],{"class":157},[137,38278,38279,38281,38283],{"class":139,"line":3896},[137,38280,4083],{"class":157},[137,38282,7821],{"class":4036},[137,38284,4053],{"class":157},[123,38286,37513],{"id":38287},"using-the-components-together-1",[128,38289,38291],{"className":4024,"code":38290,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import DataContainer from \".\u002Fcomponents\u002FDataContainer.vue\";\n    import Card from \".\u002Fcomponents\u002FCard.vue\";\n    import Details from \".\u002Fcomponents\u002FDetails.vue\";\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003CDataContainer v-slot=\"{ data }\">\n        \u003CCard :data=\"data\" \u002F>\n        \u003Cdetails :data=\"data\" \u002F>\n    \u003C\u002FDataContainer>\n\u003C\u002Ftemplate>\n",[22,38292,38293,38303,38316,38329,38342,38350,38354,38362,38378,38392,38409,38417],{"__ignoreMap":133},[137,38294,38295,38297,38299,38301],{"class":139,"line":140},[137,38296,4033],{"class":157},[137,38298,4037],{"class":4036},[137,38300,9642],{"class":147},[137,38302,4053],{"class":157},[137,38304,38305,38307,38309,38311,38314],{"class":139,"line":173},[137,38306,37677],{"class":143},[137,38308,37553],{"class":157},[137,38310,10954],{"class":143},[137,38312,38313],{"class":284}," \".\u002Fcomponents\u002FDataContainer.vue\"",[137,38315,3276],{"class":157},[137,38317,38318,38320,38322,38324,38327],{"class":139,"line":188},[137,38319,37677],{"class":143},[137,38321,37525],{"class":157},[137,38323,10954],{"class":143},[137,38325,38326],{"class":284}," \".\u002Fcomponents\u002FCard.vue\"",[137,38328,3276],{"class":157},[137,38330,38331,38333,38335,38337,38340],{"class":139,"line":269},[137,38332,37677],{"class":143},[137,38334,37539],{"class":157},[137,38336,10954],{"class":143},[137,38338,38339],{"class":284}," \".\u002Fcomponents\u002FDetails.vue\"",[137,38341,3276],{"class":157},[137,38343,38344,38346,38348],{"class":139,"line":278},[137,38345,4083],{"class":157},[137,38347,4037],{"class":4036},[137,38349,4053],{"class":157},[137,38351,38352],{"class":139,"line":291},[137,38353,516],{"emptyLinePlaceholder":515},[137,38355,38356,38358,38360],{"class":139,"line":297},[137,38357,4033],{"class":157},[137,38359,7821],{"class":4036},[137,38361,4053],{"class":157},[137,38363,38364,38366,38368,38371,38373,38376],{"class":139,"line":302},[137,38365,4072],{"class":157},[137,38367,37208],{"class":8180},[137,38369,38370],{"class":147}," v-slot",[137,38372,253],{"class":157},[137,38374,38375],{"class":284},"\"{ data }\"",[137,38377,4053],{"class":157},[137,38379,38380,38382,38384,38386,38388,38390],{"class":139,"line":662},[137,38381,9826],{"class":157},[137,38383,30584],{"class":8180},[137,38385,38265],{"class":147},[137,38387,253],{"class":157},[137,38389,38270],{"class":284},[137,38391,4078],{"class":157},[137,38393,38394,38396,38398,38400,38402,38404,38407],{"class":139,"line":667},[137,38395,9826],{"class":157},[137,38397,14264],{"class":4036},[137,38399,38265],{"class":147},[137,38401,253],{"class":157},[137,38403,38270],{"class":284},[137,38405,38406],{"class":8180}," \u002F",[137,38408,4053],{"class":157},[137,38410,38411,38413,38415],{"class":139,"line":786},[137,38412,8374],{"class":157},[137,38414,37208],{"class":8180},[137,38416,4053],{"class":157},[137,38418,38419,38421,38423],{"class":139,"line":798},[137,38420,4083],{"class":157},[137,38422,7821],{"class":4036},[137,38424,4053],{"class":157},[104,38426,38428],{"id":38427},"enhancing-the-pattern-with-modern-approaches","Enhancing the Pattern with Modern Approaches",[123,38430,38432],{"id":38431},"using-react-hooks","Using React Hooks",[27,38434,38435],{},"Encapsulate logic with a custom hook:",[128,38437,38439],{"className":130,"code":38438,"language":132,"meta":133,"style":133},"import { useState, useEffect } from \"react\";\n\nfunction useData() {\n    const [data, setData] = useState(null);\n\n    useEffect(() => {\n        setTimeout(() => {\n            setData({\n                name: \"John Doe\",\n                age: 30,\n                email: \"john.doe@example.com\",\n                address: \"123 Main St\",\n                city: \"Anytown\",\n                state: \"CA\",\n                zip: \"12345\",\n                phone: \"555-1234\",\n                occupation: \"Software Developer\",\n                company: \"Tech Corp\",\n                hobbies: [\"reading\", \"gaming\", \"hiking\"],\n                website: \"https:\u002F\u002Fjohndoe.com\",\n            });\n        }, 1000);\n    }, []);\n\n    return { data };\n}\n\nexport default useData;\n",[22,38440,38441,38454,38458,38467,38491,38495,38505,38515,38521,38529,38537,38545,38553,38561,38569,38577,38585,38593,38601,38617,38625,38629,38637,38641,38645,38652,38656,38660],{"__ignoreMap":133},[137,38442,38443,38445,38448,38450,38452],{"class":139,"line":140},[137,38444,10287],{"class":143},[137,38446,38447],{"class":157}," { useState, useEffect } ",[137,38449,10954],{"class":143},[137,38451,37226],{"class":284},[137,38453,3276],{"class":157},[137,38455,38456],{"class":139,"line":173},[137,38457,516],{"emptyLinePlaceholder":515},[137,38459,38460,38462,38465],{"class":139,"line":188},[137,38461,483],{"class":143},[137,38463,38464],{"class":147}," useData",[137,38466,275],{"class":157},[137,38468,38469,38471,38473,38475,38477,38479,38481,38483,38485,38487,38489],{"class":139,"line":269},[137,38470,4177],{"class":143},[137,38472,22130],{"class":157},[137,38474,1942],{"class":364},[137,38476,164],{"class":157},[137,38478,37259],{"class":364},[137,38480,5796],{"class":157},[137,38482,253],{"class":143},[137,38484,37266],{"class":147},[137,38486,356],{"class":157},[137,38488,11700],{"class":364},[137,38490,1502],{"class":157},[137,38492,38493],{"class":139,"line":278},[137,38494,516],{"emptyLinePlaceholder":515},[137,38496,38497,38499,38501,38503],{"class":139,"line":291},[137,38498,37281],{"class":147},[137,38500,3193],{"class":157},[137,38502,222],{"class":143},[137,38504,256],{"class":157},[137,38506,38507,38509,38511,38513],{"class":139,"line":297},[137,38508,34665],{"class":147},[137,38510,3193],{"class":157},[137,38512,222],{"class":143},[137,38514,256],{"class":157},[137,38516,38517,38519],{"class":139,"line":302},[137,38518,37307],{"class":147},[137,38520,3175],{"class":157},[137,38522,38523,38525,38527],{"class":139,"line":662},[137,38524,37314],{"class":157},[137,38526,37317],{"class":284},[137,38528,1961],{"class":157},[137,38530,38531,38533,38535],{"class":139,"line":667},[137,38532,37324],{"class":157},[137,38534,37327],{"class":364},[137,38536,1961],{"class":157},[137,38538,38539,38541,38543],{"class":139,"line":786},[137,38540,37334],{"class":157},[137,38542,37337],{"class":284},[137,38544,1961],{"class":157},[137,38546,38547,38549,38551],{"class":139,"line":798},[137,38548,37344],{"class":157},[137,38550,37347],{"class":284},[137,38552,1961],{"class":157},[137,38554,38555,38557,38559],{"class":139,"line":803},[137,38556,37354],{"class":157},[137,38558,37357],{"class":284},[137,38560,1961],{"class":157},[137,38562,38563,38565,38567],{"class":139,"line":931},[137,38564,37364],{"class":157},[137,38566,37367],{"class":284},[137,38568,1961],{"class":157},[137,38570,38571,38573,38575],{"class":139,"line":1568},[137,38572,37374],{"class":157},[137,38574,37377],{"class":284},[137,38576,1961],{"class":157},[137,38578,38579,38581,38583],{"class":139,"line":1573},[137,38580,37384],{"class":157},[137,38582,37387],{"class":284},[137,38584,1961],{"class":157},[137,38586,38587,38589,38591],{"class":139,"line":1578},[137,38588,37394],{"class":157},[137,38590,37397],{"class":284},[137,38592,1961],{"class":157},[137,38594,38595,38597,38599],{"class":139,"line":1588},[137,38596,37404],{"class":157},[137,38598,37407],{"class":284},[137,38600,1961],{"class":157},[137,38602,38603,38605,38607,38609,38611,38613,38615],{"class":139,"line":1601},[137,38604,37414],{"class":157},[137,38606,37417],{"class":284},[137,38608,164],{"class":157},[137,38610,37422],{"class":284},[137,38612,164],{"class":157},[137,38614,37427],{"class":284},[137,38616,21916],{"class":157},[137,38618,38619,38621,38623],{"class":139,"line":3802},[137,38620,37434],{"class":157},[137,38622,37437],{"class":284},[137,38624,1961],{"class":157},[137,38626,38627],{"class":139,"line":3808},[137,38628,14336],{"class":157},[137,38630,38631,38633,38635],{"class":139,"line":3822},[137,38632,34717],{"class":157},[137,38634,37450],{"class":364},[137,38636,1502],{"class":157},[137,38638,38639],{"class":139,"line":3827},[137,38640,37457],{"class":157},[137,38642,38643],{"class":139,"line":3832},[137,38644,516],{"emptyLinePlaceholder":515},[137,38646,38647,38649],{"class":139,"line":3840},[137,38648,176],{"class":143},[137,38650,38651],{"class":157}," { data };\n",[137,38653,38654],{"class":139,"line":3846},[137,38655,510],{"class":157},[137,38657,38658],{"class":139,"line":3861},[137,38659,516],{"emptyLinePlaceholder":515},[137,38661,38662,38664,38666],{"class":139,"line":3883},[137,38663,13456],{"class":143},[137,38665,21723],{"class":143},[137,38667,38668],{"class":157}," useData;\n",[123,38670,38672],{"id":38671},"using-the-react-custom-hook-in-a-component","Using the React Custom Hook in a Component",[128,38674,38676],{"className":4024,"code":38675,"language":4026,"meta":133,"style":133},"import Card from \".\u002Fcomponents\u002FCard\";\nimport Details from \".\u002Fcomponents\u002FDetails\";\nimport useData from \".\u002Fcomponents\u002FuseData\";\n\nfunction App() {\n    const { data } = useData();\n\n    return (\n        \u003C>\n            \u003CCard data={data} \u002F>\n            \u003CDetails data={data} \u002F>\n        \u003C\u002F>\n    );\n}\n\nexport default App;\n",[22,38677,38678,38683,38688,38693,38697,38702,38707,38711,38716,38722,38737,38753,38759,38763,38767,38771],{"__ignoreMap":133},[137,38679,38680],{"class":139,"line":140},[137,38681,38682],{"class":157},"import Card from \".\u002Fcomponents\u002FCard\";\n",[137,38684,38685],{"class":139,"line":173},[137,38686,38687],{"class":157},"import Details from \".\u002Fcomponents\u002FDetails\";\n",[137,38689,38690],{"class":139,"line":188},[137,38691,38692],{"class":157},"import useData from \".\u002Fcomponents\u002FuseData\";\n",[137,38694,38695],{"class":139,"line":269},[137,38696,516],{"emptyLinePlaceholder":515},[137,38698,38699],{"class":139,"line":278},[137,38700,38701],{"class":157},"function App() {\n",[137,38703,38704],{"class":139,"line":291},[137,38705,38706],{"class":157},"    const { data } = useData();\n",[137,38708,38709],{"class":139,"line":297},[137,38710,516],{"emptyLinePlaceholder":515},[137,38712,38713],{"class":139,"line":302},[137,38714,38715],{"class":157},"    return (\n",[137,38717,38718,38720],{"class":139,"line":662},[137,38719,9826],{"class":8180},[137,38721,4053],{"class":157},[137,38723,38724,38726,38728,38730,38732,38735],{"class":139,"line":667},[137,38725,23852],{"class":157},[137,38727,30584],{"class":8180},[137,38729,38067],{"class":147},[137,38731,253],{"class":157},[137,38733,38734],{"class":284},"{data}",[137,38736,4078],{"class":157},[137,38738,38739,38741,38743,38745,38747,38749,38751],{"class":139,"line":786},[137,38740,23852],{"class":157},[137,38742,36990],{"class":4036},[137,38744,38067],{"class":147},[137,38746,253],{"class":157},[137,38748,38734],{"class":284},[137,38750,38406],{"class":8180},[137,38752,4053],{"class":157},[137,38754,38755,38757],{"class":139,"line":798},[137,38756,9826],{"class":8180},[137,38758,21775],{"class":157},[137,38760,38761],{"class":139,"line":803},[137,38762,11875],{"class":157},[137,38764,38765],{"class":139,"line":931},[137,38766,510],{"class":157},[137,38768,38769],{"class":139,"line":1568},[137,38770,516],{"emptyLinePlaceholder":515},[137,38772,38773],{"class":139,"line":1573},[137,38774,38775],{"class":157},"export default App;\n",[123,38777,38779],{"id":38778},"using-vue-3-composables","Using Vue 3 Composables",[27,38781,38782],{},"Similarly, encapsulate logic with a composable function:",[128,38784,38786],{"className":130,"code":38785,"language":132,"meta":133,"style":133},"import { ref, onMounted } from \"vue\";\nconst data = ref(null);\n\nexport default function useData() {\n    onMounted(() => {\n        setTimeout(() => {\n            data.value = {\n                name: \"John Doe\",\n                age: 30,\n                email: \"john.doe@example.com\",\n                address: \"123 Main St\",\n                city: \"Anytown\",\n                state: \"CA\",\n                zip: \"12345\",\n                phone: \"555-1234\",\n                occupation: \"Software Developer\",\n                company: \"Tech Corp\",\n                hobbies: [\"reading\", \"gaming\", \"hiking\"],\n                website: \"https:\u002F\u002Fjohndoe.com\",\n            };\n        }, 1000);\n    });\n\n    return { data };\n}\n",[22,38787,38788,38800,38816,38820,38832,38842,38852,38860,38868,38876,38884,38892,38900,38908,38916,38924,38932,38940,38956,38964,38968,38976,38980,38984,38990],{"__ignoreMap":133},[137,38789,38790,38792,38794,38796,38798],{"class":139,"line":140},[137,38791,10287],{"class":143},[137,38793,11662],{"class":157},[137,38795,10954],{"class":143},[137,38797,11091],{"class":284},[137,38799,3276],{"class":157},[137,38801,38802,38804,38806,38808,38810,38812,38814],{"class":139,"line":173},[137,38803,3077],{"class":143},[137,38805,38067],{"class":364},[137,38807,151],{"class":143},[137,38809,10468],{"class":147},[137,38811,356],{"class":157},[137,38813,11700],{"class":364},[137,38815,1502],{"class":157},[137,38817,38818],{"class":139,"line":188},[137,38819,516],{"emptyLinePlaceholder":515},[137,38821,38822,38824,38826,38828,38830],{"class":139,"line":269},[137,38823,13456],{"class":143},[137,38825,21723],{"class":143},[137,38827,154],{"class":143},[137,38829,38464],{"class":147},[137,38831,275],{"class":157},[137,38833,38834,38836,38838,38840],{"class":139,"line":278},[137,38835,34654],{"class":147},[137,38837,3193],{"class":157},[137,38839,222],{"class":143},[137,38841,256],{"class":157},[137,38843,38844,38846,38848,38850],{"class":139,"line":291},[137,38845,34665],{"class":147},[137,38847,3193],{"class":157},[137,38849,222],{"class":143},[137,38851,256],{"class":157},[137,38853,38854,38856,38858],{"class":139,"line":297},[137,38855,38110],{"class":157},[137,38857,253],{"class":143},[137,38859,256],{"class":157},[137,38861,38862,38864,38866],{"class":139,"line":302},[137,38863,37314],{"class":157},[137,38865,37317],{"class":284},[137,38867,1961],{"class":157},[137,38869,38870,38872,38874],{"class":139,"line":662},[137,38871,37324],{"class":157},[137,38873,37327],{"class":364},[137,38875,1961],{"class":157},[137,38877,38878,38880,38882],{"class":139,"line":667},[137,38879,37334],{"class":157},[137,38881,37337],{"class":284},[137,38883,1961],{"class":157},[137,38885,38886,38888,38890],{"class":139,"line":786},[137,38887,37344],{"class":157},[137,38889,37347],{"class":284},[137,38891,1961],{"class":157},[137,38893,38894,38896,38898],{"class":139,"line":798},[137,38895,37354],{"class":157},[137,38897,37357],{"class":284},[137,38899,1961],{"class":157},[137,38901,38902,38904,38906],{"class":139,"line":803},[137,38903,37364],{"class":157},[137,38905,37367],{"class":284},[137,38907,1961],{"class":157},[137,38909,38910,38912,38914],{"class":139,"line":931},[137,38911,37374],{"class":157},[137,38913,37377],{"class":284},[137,38915,1961],{"class":157},[137,38917,38918,38920,38922],{"class":139,"line":1568},[137,38919,37384],{"class":157},[137,38921,37387],{"class":284},[137,38923,1961],{"class":157},[137,38925,38926,38928,38930],{"class":139,"line":1573},[137,38927,37394],{"class":157},[137,38929,37397],{"class":284},[137,38931,1961],{"class":157},[137,38933,38934,38936,38938],{"class":139,"line":1578},[137,38935,37404],{"class":157},[137,38937,37407],{"class":284},[137,38939,1961],{"class":157},[137,38941,38942,38944,38946,38948,38950,38952,38954],{"class":139,"line":1588},[137,38943,37414],{"class":157},[137,38945,37417],{"class":284},[137,38947,164],{"class":157},[137,38949,37422],{"class":284},[137,38951,164],{"class":157},[137,38953,37427],{"class":284},[137,38955,21916],{"class":157},[137,38957,38958,38960,38962],{"class":139,"line":1601},[137,38959,37434],{"class":157},[137,38961,37437],{"class":284},[137,38963,1961],{"class":157},[137,38965,38966],{"class":139,"line":3802},[137,38967,38223],{"class":157},[137,38969,38970,38972,38974],{"class":139,"line":3808},[137,38971,34717],{"class":157},[137,38973,37450],{"class":364},[137,38975,1502],{"class":157},[137,38977,38978],{"class":139,"line":3822},[137,38979,2832],{"class":157},[137,38981,38982],{"class":139,"line":3827},[137,38983,516],{"emptyLinePlaceholder":515},[137,38985,38986,38988],{"class":139,"line":3832},[137,38987,176],{"class":143},[137,38989,38651],{"class":157},[137,38991,38992],{"class":139,"line":3840},[137,38993,510],{"class":157},[123,38995,38997],{"id":38996},"using-the-vue-composable-in-a-component","Using the Vue Composable in a Component",[128,38999,39001],{"className":4024,"code":39000,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import Card from \".\u002Fcomponents\u002FCard.vue\";\n    import Details from \".\u002Fcomponents\u002FDetails.vue\";\n    import useData from \".\u002Fcomponents\u002FuseData\";\n\n    const { data } = useData();\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003CCard :data=\"data\" \u002F>\n    \u003Cdetails :data=\"data\" \u002F>\n\u003C\u002Ftemplate>\n",[22,39002,39003,39013,39025,39037,39051,39055,39071,39079,39083,39091,39105,39121],{"__ignoreMap":133},[137,39004,39005,39007,39009,39011],{"class":139,"line":140},[137,39006,4033],{"class":157},[137,39008,4037],{"class":4036},[137,39010,9642],{"class":147},[137,39012,4053],{"class":157},[137,39014,39015,39017,39019,39021,39023],{"class":139,"line":173},[137,39016,37677],{"class":143},[137,39018,37525],{"class":157},[137,39020,10954],{"class":143},[137,39022,38326],{"class":284},[137,39024,3276],{"class":157},[137,39026,39027,39029,39031,39033,39035],{"class":139,"line":188},[137,39028,37677],{"class":143},[137,39030,37539],{"class":157},[137,39032,10954],{"class":143},[137,39034,38339],{"class":284},[137,39036,3276],{"class":157},[137,39038,39039,39041,39044,39046,39049],{"class":139,"line":269},[137,39040,37677],{"class":143},[137,39042,39043],{"class":157}," useData ",[137,39045,10954],{"class":143},[137,39047,39048],{"class":284}," \".\u002Fcomponents\u002FuseData\"",[137,39050,3276],{"class":157},[137,39052,39053],{"class":139,"line":278},[137,39054,516],{"emptyLinePlaceholder":515},[137,39056,39057,39059,39061,39063,39065,39067,39069],{"class":139,"line":291},[137,39058,4177],{"class":143},[137,39060,8906],{"class":157},[137,39062,1942],{"class":364},[137,39064,8911],{"class":157},[137,39066,253],{"class":143},[137,39068,38464],{"class":147},[137,39070,924],{"class":157},[137,39072,39073,39075,39077],{"class":139,"line":297},[137,39074,4083],{"class":157},[137,39076,4037],{"class":4036},[137,39078,4053],{"class":157},[137,39080,39081],{"class":139,"line":302},[137,39082,516],{"emptyLinePlaceholder":515},[137,39084,39085,39087,39089],{"class":139,"line":662},[137,39086,4033],{"class":157},[137,39088,7821],{"class":4036},[137,39090,4053],{"class":157},[137,39092,39093,39095,39097,39099,39101,39103],{"class":139,"line":667},[137,39094,4072],{"class":157},[137,39096,30584],{"class":8180},[137,39098,38265],{"class":147},[137,39100,253],{"class":157},[137,39102,38270],{"class":284},[137,39104,4078],{"class":157},[137,39106,39107,39109,39111,39113,39115,39117,39119],{"class":139,"line":786},[137,39108,4072],{"class":157},[137,39110,14264],{"class":4036},[137,39112,38265],{"class":147},[137,39114,253],{"class":157},[137,39116,38270],{"class":284},[137,39118,38406],{"class":8180},[137,39120,4053],{"class":157},[137,39122,39123,39125,39127],{"class":139,"line":798},[137,39124,4083],{"class":157},[137,39126,7821],{"class":4036},[137,39128,4053],{"class":157},[104,39130,39131],{"id":2566},[42,39132,2567],{},[27,39134,4737,39135,39137,39138,114,39140,39143,39144,39147],{},[42,39136,36782],{}," remains a powerful approach for separating UI and logic in modern frontend development. While traditional container components are effective, ",[42,39139,36786],{},[42,39141,39142],{},"Vue 3 Composables"," provide an even more elegant solution through modular, reusable functions. By moving logic into dedicated functions like ",[22,39145,39146],{},"useData",", we gain several advantages:",[1003,39149,39150,39153,39156],{},[1006,39151,39152],{},"Better reusability across components",[1006,39154,39155],{},"Cleaner code with less boilerplate",[1006,39157,39158],{},"Improved testing and scalability",[27,39160,39161,39162,3596,39164,3955,39166,3596,39168,39171],{},"For modern frontend projects, embracing ",[42,39163,36801],{},[42,39165,36809],{},[42,39167,26585],{},[42,39169,39170],{},"3 Composables"," can significantly enhance your development workflow and align with contemporary component design principles.",[27,39173,36698,39174,1017],{},[45,39175,10647],{"href":39176,"target":2716,"rel":39177},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fthe-container-presentational-pattern-with-react-and-vue",[2718,2719],[2617,39179,39180],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":133,"searchDepth":173,"depth":173,"links":39182},[39183,39184,39193,39202,39208],{"id":36816,"depth":173,"text":36819},{"id":36866,"depth":173,"text":36869,"children":39185},[39186,39188,39190,39192],{"id":36872,"depth":188,"text":39187},"Presentational Component - Card.jsx",{"id":36981,"depth":188,"text":39189},"Presentational Component - Details.jsx",{"id":37199,"depth":188,"text":39191},"Container Component - DataContainer.jsx",{"id":37512,"depth":188,"text":37513},{"id":37645,"depth":173,"text":37646,"children":39194},[39195,39197,39199,39201],{"id":37649,"depth":188,"text":39196},"Presentational Component - Card.vue",{"id":37785,"depth":188,"text":39198},"Presentational Component - Details.vue",{"id":38021,"depth":188,"text":39200},"Container Component - DataContainer.vue",{"id":38287,"depth":188,"text":37513},{"id":38427,"depth":173,"text":38428,"children":39203},[39204,39205,39206,39207],{"id":38431,"depth":188,"text":38432},{"id":38671,"depth":188,"text":38672},{"id":38778,"depth":188,"text":38779},{"id":38996,"depth":188,"text":38997},{"id":2566,"depth":173,"text":2567},"This blog post dives into the Container\u002FPresentational Pattern, demonstrating how to create cleaner, more modular, and reusable code with React Hooks and Vue 3 Composables. Explore how to modernise this classic frontend pattern by transitioning from traditional implementations to scalable, maintainable solutions. Perfect for developers looking to streamline logic, enhance maintainability, and build better React and Vue applications.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1736332534\u002Fblog\u002Fthe-container-presentational-pattern-with-react-and-vue\u002Fthe-container-presentational-pattern-with-react-and-vue_tx5c9t",[29177,8,8448,5300,5299,36782,36786,39142,39212,39213,39214,39215,39216,39217,39218,39219,39220,39221,39222,39223,39224,39225,39226],"frontend development patterns","modern frontend design","reusable components","state management","React custom hooks","Vue composable functions","separating UI and logic","modular components","clean code in React","clean code in Vue","frontend architecture","React patterns","Vue 3 patterns","scalable frontend design","reusable frontend logic",{},"\u002F2025\u002F01\u002F09\u002Fthe-container-presentational-pattern-with-react-and-vue","9th January 2025",{"title":36746,"description":39209},"2025\u002F01\u002F09\u002Fthe-container-presentational-pattern-with-react-and-vue","eo3Z_BSLsQiaQPZ9RwAWdkYCVX1hhGob0JAutEDBGQo",{"id":39234,"title":39235,"articleTags":39236,"author":11,"blog":12,"body":39237,"description":44925,"extension":2649,"image":44926,"keywords":44927,"meta":44952,"navigation":515,"path":44953,"published":44954,"readTime":798,"seo":44955,"stem":44956,"type":2662,"__hash__":44957},"content\u002F2025\u002F01\u002F15\u002Fa-beginners-guide-to-vue-for-react-developers.md","A Beginner's Guide to Vue for React Developers",[9,8,29177],{"type":14,"value":39238,"toc":44865},[39239,39242,39256,39258,39262,39267,39270,39274,39277,39280,39326,39328,39331,39383,39388,39412,39416,39419,39425,39428,39433,39507,39512,39593,39600,39604,39607,39612,39619,39682,39687,39729,39732,39741,39852,39856,39871,39875,39878,39989,39994,40009,40016,40019,40024,40031,40036,40072,40076,40138,40143,40150,40244,40248,40251,40258,40406,40413,40689,40696,40887,40890,40896,41023,41030,41273,41279,41472,41476,41522,41529,41534,41537,41540,41722,41728,41731,41858,41863,41871,41875,41878,41885,42008,42011,42019,42148,42152,42162,42166,42169,42174,42298,42301,42307,42438,42442,42450,42454,42457,42460,42522,42525,42534,42538,42548,42638,42642,42647,42653,42658,42709,42714,42764,42769,42772,42776,42847,42851,42922,42926,42947,42951,42954,42957,43057,43060,43067,43075,43130,43134,43219,43224,43227,43233,43306,43310,43392,43396,43403,43407,43410,43413,43507,43510,43516,43521,43605,43609,43710,43714,43721,43725,43728,43734,43883,43886,43889,44048,44052,44068,44072,44075,44078,44209,44212,44215,44220,44296,44301,44400,44404,44423,44427,44433,44438,44539,44544,44685,44690,44830,44843,44847,44850,44853,44856,44862],[17,39240,39235],{"id":39241},"a-beginners-guide-to-vue-for-react-developers",[27,39243,39244],{},[30,39245,39246,36,39248,40,39250],{},[33,39247],{"value":35},[33,39249],{"value":39},[42,39251,39252],{},[45,39253,39254],{"href":47},[33,39255],{"value":50},[52,39257],{":tags":54},[56,39259],{":audio-src":39260,":transcript-src":39261},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F01\u002F15\u002Fa-beginners-guide-to-vue-for-react-developers\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F01\u002F15\u002Fa-beginners-guide-to-vue-for-react-developers\u002Fsummary.json",[27,39263,39264],{},[63,39265],{"alt":12847,"src":39266},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1736935516\u002Fblog\u002Fa-beginners-guide-to-vue-for-react-developers\u002FA_scenic_landscape_featuring_a_slightly_curved_road_that_extends_infinitely_into_the_horizon._The_road_starts_with_a_vibrant_blue_hue_and_gradually_tr",[27,39268,39269],{},"Vue is a frontend framework that shares many concepts with React while taking its own unique approach. This guide will walk you through Vue's key concepts and compare them to their React equivalents. By the end, you'll have a solid foundation for working with Vue.",[104,39271,39273],{"id":39272},"setting-up-a-component","Setting Up a Component",[123,39275,36801],{"id":39276},"react",[27,39278,39279],{},"In React, a functional component is created like this:",[128,39281,39283],{"className":36884,"code":39282,"language":29196,"meta":133,"style":133},"function ExampleComponent() {\n    return \u003Ch1>Hello, React!\u003C\u002Fh1>;\n}\n\nexport default ExampleComponent;\n",[22,39284,39285,39294,39309,39313,39317],{"__ignoreMap":133},[137,39286,39287,39289,39292],{"class":139,"line":140},[137,39288,483],{"class":143},[137,39290,39291],{"class":147}," ExampleComponent",[137,39293,275],{"class":157},[137,39295,39296,39298,39300,39302,39305,39307],{"class":139,"line":173},[137,39297,176],{"class":143},[137,39299,29304],{"class":157},[137,39301,17],{"class":4036},[137,39303,39304],{"class":157},">Hello, React!\u003C\u002F",[137,39306,17],{"class":4036},[137,39308,29320],{"class":157},[137,39310,39311],{"class":139,"line":188},[137,39312,510],{"class":157},[137,39314,39315],{"class":139,"line":269},[137,39316,516],{"emptyLinePlaceholder":515},[137,39318,39319,39321,39323],{"class":139,"line":278},[137,39320,13456],{"class":143},[137,39322,21723],{"class":143},[137,39324,39325],{"class":157}," ExampleComponent;\n",[123,39327,26585],{"id":27822},[27,39329,39330],{},"In Vue, components are defined with a template and a script:",[128,39332,39334],{"className":4024,"code":39333,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Ch1>Hello, Vue!\u003C\u002Fh1>\n\u003C\u002Ftemplate>\n",[22,39335,39336,39350,39354,39362,39375],{"__ignoreMap":133},[137,39337,39338,39340,39342,39344,39346,39348],{"class":139,"line":140},[137,39339,4033],{"class":157},[137,39341,4037],{"class":4036},[137,39343,9642],{"class":147},[137,39345,4048],{"class":157},[137,39347,4037],{"class":4036},[137,39349,4053],{"class":157},[137,39351,39352],{"class":139,"line":173},[137,39353,516],{"emptyLinePlaceholder":515},[137,39355,39356,39358,39360],{"class":139,"line":188},[137,39357,4033],{"class":157},[137,39359,7821],{"class":4036},[137,39361,4053],{"class":157},[137,39363,39364,39366,39368,39371,39373],{"class":139,"line":269},[137,39365,4072],{"class":157},[137,39367,17],{"class":4036},[137,39369,39370],{"class":157},">Hello, Vue!\u003C\u002F",[137,39372,17],{"class":4036},[137,39374,4053],{"class":157},[137,39376,39377,39379,39381],{"class":139,"line":278},[137,39378,4083],{"class":157},[137,39380,7821],{"class":4036},[137,39382,4053],{"class":157},[27,39384,39385],{},[42,39386,39387],{},"Key Differences:",[1003,39389,39390,39396,39404],{},[1006,39391,39392,39393,39395],{},"Vue components are defined in files with the ",[22,39394,7575],{}," extension.",[1006,39397,39398,39399,39401,39402,4409],{},"Vue separates logic (",[22,39400,4037],{},") and markup (",[22,39403,7821],{},[1006,39405,39406,39407,39409,39410,10277],{},"Vue doesn't require JSX, instead, you write HTML-like syntax directly in the ",[22,39408,7821],{}," section of your ",[22,39411,7575],{},[104,39413,39415],{"id":39414},"styling-components","Styling Components",[27,39417,39418],{},"Now that we've set up our components, let’s explore how to style them. Styling components in React and Vue can be done in multiple ways, including:",[123,39420,39422],{"id":39421},"inline-styles",[42,39423,39424],{},"Inline Styles",[27,39426,39427],{},"Both React and Vue support inline styles, though their syntax differs slightly.",[123,39429,39431],{"id":39430},"react-1",[42,39432,36801],{},[128,39434,39436],{"className":36884,"code":39435,"language":29196,"meta":133,"style":133},"function ExampleComponent() {\n    const inlineStyle = { color: \"blue\", fontSize: \"20px\" };\n\n    return \u003Ch1 style={inlineStyle}>Hello, React!\u003C\u002Fh1>;\n}\n\nexport default ExampleComponent;\n",[22,39437,39438,39446,39468,39472,39491,39495,39499],{"__ignoreMap":133},[137,39439,39440,39442,39444],{"class":139,"line":140},[137,39441,483],{"class":143},[137,39443,39291],{"class":147},[137,39445,275],{"class":157},[137,39447,39448,39450,39453,39455,39458,39460,39463,39466],{"class":139,"line":173},[137,39449,4177],{"class":143},[137,39451,39452],{"class":364}," inlineStyle",[137,39454,151],{"class":143},[137,39456,39457],{"class":157}," { color: ",[137,39459,7905],{"class":284},[137,39461,39462],{"class":157},", fontSize: ",[137,39464,39465],{"class":284},"\"20px\"",[137,39467,32107],{"class":157},[137,39469,39470],{"class":139,"line":188},[137,39471,516],{"emptyLinePlaceholder":515},[137,39473,39474,39476,39478,39480,39482,39484,39487,39489],{"class":139,"line":269},[137,39475,176],{"class":143},[137,39477,29304],{"class":157},[137,39479,17],{"class":4036},[137,39481,3755],{"class":147},[137,39483,253],{"class":143},[137,39485,39486],{"class":157},"{inlineStyle}>Hello, React!\u003C\u002F",[137,39488,17],{"class":4036},[137,39490,29320],{"class":157},[137,39492,39493],{"class":139,"line":278},[137,39494,510],{"class":157},[137,39496,39497],{"class":139,"line":291},[137,39498,516],{"emptyLinePlaceholder":515},[137,39500,39501,39503,39505],{"class":139,"line":297},[137,39502,13456],{"class":143},[137,39504,21723],{"class":143},[137,39506,39325],{"class":157},[123,39508,39510],{"id":39509},"vue-1",[42,39511,26585],{},[128,39513,39515],{"className":4024,"code":39514,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    const inlineStyle = { color: \"blue\", fontSize: \"20px\" };\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Ch1 :style=\"inlineStyle\">Hello, Vue!\u003C\u002Fh1>\n\u003C\u002Ftemplate>\n",[22,39516,39517,39527,39545,39553,39557,39565,39585],{"__ignoreMap":133},[137,39518,39519,39521,39523,39525],{"class":139,"line":140},[137,39520,4033],{"class":157},[137,39522,4037],{"class":4036},[137,39524,9642],{"class":147},[137,39526,4053],{"class":157},[137,39528,39529,39531,39533,39535,39537,39539,39541,39543],{"class":139,"line":173},[137,39530,4177],{"class":143},[137,39532,39452],{"class":364},[137,39534,151],{"class":143},[137,39536,39457],{"class":157},[137,39538,7905],{"class":284},[137,39540,39462],{"class":157},[137,39542,39465],{"class":284},[137,39544,32107],{"class":157},[137,39546,39547,39549,39551],{"class":139,"line":188},[137,39548,4083],{"class":157},[137,39550,4037],{"class":4036},[137,39552,4053],{"class":157},[137,39554,39555],{"class":139,"line":269},[137,39556,516],{"emptyLinePlaceholder":515},[137,39558,39559,39561,39563],{"class":139,"line":278},[137,39560,4033],{"class":157},[137,39562,7821],{"class":4036},[137,39564,4053],{"class":157},[137,39566,39567,39569,39571,39574,39576,39579,39581,39583],{"class":139,"line":291},[137,39568,4072],{"class":157},[137,39570,17],{"class":4036},[137,39572,39573],{"class":147}," :style",[137,39575,253],{"class":157},[137,39577,39578],{"class":284},"\"inlineStyle\"",[137,39580,39370],{"class":157},[137,39582,17],{"class":4036},[137,39584,4053],{"class":157},[137,39586,39587,39589,39591],{"class":139,"line":297},[137,39588,4083],{"class":157},[137,39590,7821],{"class":4036},[137,39592,4053],{"class":157},[27,39594,39595,39596,39599],{},"In Vue, ",[22,39597,39598],{},":style"," is a directive used for binding styles.",[123,39601,39603],{"id":39602},"external-styles","External Styles",[27,39605,39606],{},"External styles involve creating a separate CSS file and importing it.",[123,39608,39610],{"id":39609},"react-2",[42,39611,36801],{},[27,39613,39614,39615,39618],{},"You can create a ",[22,39616,39617],{},"ExampleComponent.css"," file and import it into your component:",[128,39620,39622],{"className":36884,"code":39621,"language":29196,"meta":133,"style":133},"import \".\u002FExampleComponent.css\";\n\nfunction ExampleComponent() {\n    return \u003Ch1 className=\"header\">Hello, React!\u003C\u002Fh1>;\n}\n\nexport default ExampleComponent;\n",[22,39623,39624,39633,39637,39645,39666,39670,39674],{"__ignoreMap":133},[137,39625,39626,39628,39631],{"class":139,"line":140},[137,39627,10287],{"class":143},[137,39629,39630],{"class":284}," \".\u002FExampleComponent.css\"",[137,39632,3276],{"class":157},[137,39634,39635],{"class":139,"line":173},[137,39636,516],{"emptyLinePlaceholder":515},[137,39638,39639,39641,39643],{"class":139,"line":188},[137,39640,483],{"class":143},[137,39642,39291],{"class":147},[137,39644,275],{"class":157},[137,39646,39647,39649,39651,39653,39655,39657,39660,39662,39664],{"class":139,"line":269},[137,39648,176],{"class":143},[137,39650,29304],{"class":157},[137,39652,17],{"class":4036},[137,39654,36916],{"class":147},[137,39656,253],{"class":143},[137,39658,39659],{"class":284},"\"header\"",[137,39661,39304],{"class":157},[137,39663,17],{"class":4036},[137,39665,29320],{"class":157},[137,39667,39668],{"class":139,"line":278},[137,39669,510],{"class":157},[137,39671,39672],{"class":139,"line":291},[137,39673,516],{"emptyLinePlaceholder":515},[137,39675,39676,39678,39680],{"class":139,"line":297},[137,39677,13456],{"class":143},[137,39679,21723],{"class":143},[137,39681,39325],{"class":157},[27,39683,39684],{},[42,39685,39686],{},"ExampleComponent.css:",[128,39688,39690],{"className":23162,"code":39689,"language":23164,"meta":133,"style":133},".header {\n    color: blue;\n    font-size: 20px;\n}\n",[22,39691,39692,39699,39711,39725],{"__ignoreMap":133},[137,39693,39694,39697],{"class":139,"line":140},[137,39695,39696],{"class":147},".header",[137,39698,256],{"class":157},[137,39700,39701,39704,39706,39709],{"class":139,"line":173},[137,39702,39703],{"class":364},"    color",[137,39705,726],{"class":157},[137,39707,39708],{"class":364},"blue",[137,39710,3276],{"class":157},[137,39712,39713,39716,39718,39720,39723],{"class":139,"line":188},[137,39714,39715],{"class":364},"    font-size",[137,39717,726],{"class":157},[137,39719,14727],{"class":364},[137,39721,39722],{"class":143},"px",[137,39724,3276],{"class":157},[137,39726,39727],{"class":139,"line":269},[137,39728,510],{"class":157},[123,39730,26585],{"id":39731},"vue-2",[27,39733,39734,39735,39737,39738,39740],{},"In Vue, styles can be added directly to the ",[22,39736,7575],{}," file in a ",[22,39739,3972],{}," block:",[128,39742,39744],{"className":4024,"code":39743,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Ch1 class=\"header\">Hello, Vue!\u003C\u002Fh1>\n\u003C\u002Ftemplate>\n\n\u003Cstyle>\n    .header {\n        color: blue;\n        font-size: 20px;\n    }\n\u003C\u002Fstyle>\n",[22,39745,39746,39760,39764,39772,39790,39798,39802,39810,39817,39827,39840,39844],{"__ignoreMap":133},[137,39747,39748,39750,39752,39754,39756,39758],{"class":139,"line":140},[137,39749,4033],{"class":157},[137,39751,4037],{"class":4036},[137,39753,9642],{"class":147},[137,39755,4048],{"class":157},[137,39757,4037],{"class":4036},[137,39759,4053],{"class":157},[137,39761,39762],{"class":139,"line":173},[137,39763,516],{"emptyLinePlaceholder":515},[137,39765,39766,39768,39770],{"class":139,"line":188},[137,39767,4033],{"class":157},[137,39769,7821],{"class":4036},[137,39771,4053],{"class":157},[137,39773,39774,39776,39778,39780,39782,39784,39786,39788],{"class":139,"line":269},[137,39775,4072],{"class":157},[137,39777,17],{"class":4036},[137,39779,7832],{"class":147},[137,39781,253],{"class":157},[137,39783,39659],{"class":284},[137,39785,39370],{"class":157},[137,39787,17],{"class":4036},[137,39789,4053],{"class":157},[137,39791,39792,39794,39796],{"class":139,"line":278},[137,39793,4083],{"class":157},[137,39795,7821],{"class":4036},[137,39797,4053],{"class":157},[137,39799,39800],{"class":139,"line":291},[137,39801,516],{"emptyLinePlaceholder":515},[137,39803,39804,39806,39808],{"class":139,"line":297},[137,39805,4033],{"class":157},[137,39807,2617],{"class":4036},[137,39809,4053],{"class":157},[137,39811,39812,39815],{"class":139,"line":302},[137,39813,39814],{"class":147},"    .header",[137,39816,256],{"class":157},[137,39818,39819,39821,39823,39825],{"class":139,"line":662},[137,39820,7784],{"class":364},[137,39822,726],{"class":157},[137,39824,39708],{"class":364},[137,39826,3276],{"class":157},[137,39828,39829,39832,39834,39836,39838],{"class":139,"line":667},[137,39830,39831],{"class":364},"        font-size",[137,39833,726],{"class":157},[137,39835,14727],{"class":364},[137,39837,39722],{"class":143},[137,39839,3276],{"class":157},[137,39841,39842],{"class":139,"line":786},[137,39843,294],{"class":157},[137,39845,39846,39848,39850],{"class":139,"line":798},[137,39847,4083],{"class":157},[137,39849,2617],{"class":4036},[137,39851,4053],{"class":157},[27,39853,39854],{},[42,39855,39387],{},[1003,39857,39858],{},[1006,39859,39860,39861,39863,39864,39867,39868,39870],{},"Vue uses ",[22,39862,3635],{}," attribute for CSS classes, while React uses ",[22,39865,39866],{},"className"," (since ",[22,39869,3635],{}," is a reserved word in JavaScript).",[123,39872,39874],{"id":39873},"scoped-styles-vue-specific","Scoped Styles (Vue-Specific)",[27,39876,39877],{},"Vue provides a scoped styling feature to limit styles to the component they belong to.",[128,39879,39881],{"className":4024,"code":39880,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Ch1 class=\"header\">Hello, Vue!\u003C\u002Fh1>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n    .header {\n        color: blue;\n        font-size: 20px;\n    }\n\u003C\u002Fstyle>\n",[22,39882,39883,39897,39901,39909,39927,39935,39939,39949,39955,39965,39977,39981],{"__ignoreMap":133},[137,39884,39885,39887,39889,39891,39893,39895],{"class":139,"line":140},[137,39886,4033],{"class":157},[137,39888,4037],{"class":4036},[137,39890,9642],{"class":147},[137,39892,4048],{"class":157},[137,39894,4037],{"class":4036},[137,39896,4053],{"class":157},[137,39898,39899],{"class":139,"line":173},[137,39900,516],{"emptyLinePlaceholder":515},[137,39902,39903,39905,39907],{"class":139,"line":188},[137,39904,4033],{"class":157},[137,39906,7821],{"class":4036},[137,39908,4053],{"class":157},[137,39910,39911,39913,39915,39917,39919,39921,39923,39925],{"class":139,"line":269},[137,39912,4072],{"class":157},[137,39914,17],{"class":4036},[137,39916,7832],{"class":147},[137,39918,253],{"class":157},[137,39920,39659],{"class":284},[137,39922,39370],{"class":157},[137,39924,17],{"class":4036},[137,39926,4053],{"class":157},[137,39928,39929,39931,39933],{"class":139,"line":278},[137,39930,4083],{"class":157},[137,39932,7821],{"class":4036},[137,39934,4053],{"class":157},[137,39936,39937],{"class":139,"line":291},[137,39938,516],{"emptyLinePlaceholder":515},[137,39940,39941,39943,39945,39947],{"class":139,"line":297},[137,39942,4033],{"class":157},[137,39944,2617],{"class":4036},[137,39946,7770],{"class":147},[137,39948,4053],{"class":157},[137,39950,39951,39953],{"class":139,"line":302},[137,39952,39814],{"class":147},[137,39954,256],{"class":157},[137,39956,39957,39959,39961,39963],{"class":139,"line":662},[137,39958,7784],{"class":364},[137,39960,726],{"class":157},[137,39962,39708],{"class":364},[137,39964,3276],{"class":157},[137,39966,39967,39969,39971,39973,39975],{"class":139,"line":667},[137,39968,39831],{"class":364},[137,39970,726],{"class":157},[137,39972,14727],{"class":364},[137,39974,39722],{"class":143},[137,39976,3276],{"class":157},[137,39978,39979],{"class":139,"line":786},[137,39980,294],{"class":157},[137,39982,39983,39985,39987],{"class":139,"line":798},[137,39984,4083],{"class":157},[137,39986,2617],{"class":4036},[137,39988,4053],{"class":157},[27,39990,39991],{},[42,39992,39993],{},"What are scoped styles in Vue?",[1003,39995,39996,40006],{},[1006,39997,39998,39999,40002,40003,40005],{},"Adding the ",[22,40000,40001],{},"scoped"," attribute to a ",[22,40004,2617],{}," tag in Vue automatically assigns unique attributes to the component's HTML elements, ensuring styles only apply within that component.",[1006,40007,40008],{},"This prevents style conflicts across components while maintaining all styling code in a single file.",[123,40010,40012,40015],{"id":40011},"reacts-approach-to-scoped-styles",[42,40013,40014],{},"React's Approach to"," Scoped Styles",[27,40017,40018],{},"While React doesn't include built-in scoped styles, you can achieve similar functionality natively with CSS Modules or by using the styled-components library to isolate styles to specific components.",[27,40020,40021],{},[42,40022,40023],{},"React CSS Modules:",[27,40025,40026,40027,40030],{},"React supports ",[42,40028,40029],{},"CSS Modules",", which are a way to scope CSS to a specific component. With CSS Modules, the class names are automatically locally scoped by appending unique names. Here’s an example:",[27,40032,40033],{},[42,40034,40035],{},"Card.module.css",[128,40037,40038],{"className":23162,"code":39689,"language":23164,"meta":133,"style":133},[22,40039,40040,40046,40056,40068],{"__ignoreMap":133},[137,40041,40042,40044],{"class":139,"line":140},[137,40043,39696],{"class":147},[137,40045,256],{"class":157},[137,40047,40048,40050,40052,40054],{"class":139,"line":173},[137,40049,39703],{"class":364},[137,40051,726],{"class":157},[137,40053,39708],{"class":364},[137,40055,3276],{"class":157},[137,40057,40058,40060,40062,40064,40066],{"class":139,"line":188},[137,40059,39715],{"class":364},[137,40061,726],{"class":157},[137,40063,14727],{"class":364},[137,40065,39722],{"class":143},[137,40067,3276],{"class":157},[137,40069,40070],{"class":139,"line":269},[137,40071,510],{"class":157},[27,40073,40074],{},[42,40075,36876],{},[128,40077,40079],{"className":36884,"code":40078,"language":29196,"meta":133,"style":133},"import styles from \".\u002FCard.module.css\";\n\nconst Card = () => \u003Cdiv className={styles.header}>Hello, React!\u003C\u002Fdiv>;\n\nexport default Card;\n",[22,40080,40081,40095,40099,40126,40130],{"__ignoreMap":133},[137,40082,40083,40085,40088,40090,40093],{"class":139,"line":140},[137,40084,10287],{"class":143},[137,40086,40087],{"class":157}," styles ",[137,40089,10954],{"class":143},[137,40091,40092],{"class":284}," \".\u002FCard.module.css\"",[137,40094,3276],{"class":157},[137,40096,40097],{"class":139,"line":173},[137,40098,516],{"emptyLinePlaceholder":515},[137,40100,40101,40103,40105,40107,40109,40111,40113,40115,40117,40119,40122,40124],{"class":139,"line":188},[137,40102,3077],{"class":143},[137,40104,30397],{"class":147},[137,40106,151],{"class":143},[137,40108,1484],{"class":157},[137,40110,222],{"class":143},[137,40112,29304],{"class":157},[137,40114,8330],{"class":4036},[137,40116,36916],{"class":147},[137,40118,253],{"class":143},[137,40120,40121],{"class":157},"{styles.header}>Hello, React!\u003C\u002F",[137,40123,8330],{"class":4036},[137,40125,29320],{"class":157},[137,40127,40128],{"class":139,"line":269},[137,40129,516],{"emptyLinePlaceholder":515},[137,40131,40132,40134,40136],{"class":139,"line":278},[137,40133,13456],{"class":143},[137,40135,21723],{"class":143},[137,40137,36978],{"class":157},[27,40139,40140],{},[42,40141,40142],{},"Styled-Components (CSS-in-JS)",[27,40144,40145,40146,40149],{},"For a more dynamic and scoped styling experience, you can use libraries like ",[42,40147,40148],{},"Styled-Components",". It allows you to define component-scoped styles directly within your JavaScript files:",[128,40151,40153],{"className":36884,"code":40152,"language":29196,"meta":133,"style":133},"import styled from \"styled-components\";\n\nconst Header = styled.h1`\n    color: blue;\n    font-size: 20px;\n`;\n\nconst Card = () => \u003CHeader>Hello, React!\u003C\u002FHeader>;\n\nexport default Card;\n",[22,40154,40155,40169,40173,40189,40194,40199,40205,40209,40232,40236],{"__ignoreMap":133},[137,40156,40157,40159,40162,40164,40167],{"class":139,"line":140},[137,40158,10287],{"class":143},[137,40160,40161],{"class":157}," styled ",[137,40163,10954],{"class":143},[137,40165,40166],{"class":284}," \"styled-components\"",[137,40168,3276],{"class":157},[137,40170,40171],{"class":139,"line":173},[137,40172,516],{"emptyLinePlaceholder":515},[137,40174,40175,40177,40180,40182,40185,40187],{"class":139,"line":188},[137,40176,3077],{"class":143},[137,40178,40179],{"class":364}," Header",[137,40181,151],{"class":143},[137,40183,40184],{"class":157}," styled.",[137,40186,17],{"class":147},[137,40188,22062],{"class":284},[137,40190,40191],{"class":139,"line":269},[137,40192,40193],{"class":284},"    color: blue;\n",[137,40195,40196],{"class":139,"line":278},[137,40197,40198],{"class":284},"    font-size: 20px;\n",[137,40200,40201,40203],{"class":139,"line":291},[137,40202,22056],{"class":284},[137,40204,3276],{"class":157},[137,40206,40207],{"class":139,"line":297},[137,40208,516],{"emptyLinePlaceholder":515},[137,40210,40211,40213,40215,40217,40219,40221,40223,40226,40228,40230],{"class":139,"line":302},[137,40212,3077],{"class":143},[137,40214,30397],{"class":147},[137,40216,151],{"class":143},[137,40218,1484],{"class":157},[137,40220,222],{"class":143},[137,40222,29304],{"class":157},[137,40224,40225],{"class":364},"Header",[137,40227,39304],{"class":157},[137,40229,40225],{"class":364},[137,40231,29320],{"class":157},[137,40233,40234],{"class":139,"line":662},[137,40235,516],{"emptyLinePlaceholder":515},[137,40237,40238,40240,40242],{"class":139,"line":667},[137,40239,13456],{"class":143},[137,40241,21723],{"class":143},[137,40243,36978],{"class":157},[104,40245,40247],{"id":40246},"state-management-with-reactivity","State Management with Reactivity",[123,40249,36801],{"id":40250},"react-3",[27,40252,40253,40254,40257],{},"React uses ",[22,40255,40256],{},"useState"," for state management:",[128,40259,40261],{"className":36884,"code":40260,"language":29196,"meta":133,"style":133},"import { useState } from \"react\";\n\nfunction ExampleComponent() {\n    const [count, setCount] = useState(0);\n\n    return (\n        \u003Cdiv>\n            \u003Cp>Count: {count}\u003C\u002Fp>\n            \u003Cbutton onClick={() => setCount(count + 1)}>Increment\u003C\u002Fbutton>\n        \u003C\u002Fdiv>\n    );\n}\n\nexport default ExampleComponent;\n",[22,40262,40263,40276,40280,40288,40314,40318,40324,40332,40345,40378,40386,40390,40394,40398],{"__ignoreMap":133},[137,40264,40265,40267,40270,40272,40274],{"class":139,"line":140},[137,40266,10287],{"class":143},[137,40268,40269],{"class":157}," { useState } ",[137,40271,10954],{"class":143},[137,40273,37226],{"class":284},[137,40275,3276],{"class":157},[137,40277,40278],{"class":139,"line":173},[137,40279,516],{"emptyLinePlaceholder":515},[137,40281,40282,40284,40286],{"class":139,"line":188},[137,40283,483],{"class":143},[137,40285,39291],{"class":147},[137,40287,275],{"class":157},[137,40289,40290,40292,40294,40297,40299,40302,40304,40306,40308,40310,40312],{"class":139,"line":269},[137,40291,4177],{"class":143},[137,40293,22130],{"class":157},[137,40295,40296],{"class":364},"count",[137,40298,164],{"class":157},[137,40300,40301],{"class":364},"setCount",[137,40303,5796],{"class":157},[137,40305,253],{"class":143},[137,40307,37266],{"class":147},[137,40309,356],{"class":157},[137,40311,6044],{"class":364},[137,40313,1502],{"class":157},[137,40315,40316],{"class":139,"line":278},[137,40317,516],{"emptyLinePlaceholder":515},[137,40319,40320,40322],{"class":139,"line":291},[137,40321,176],{"class":143},[137,40323,30009],{"class":157},[137,40325,40326,40328,40330],{"class":139,"line":297},[137,40327,9826],{"class":157},[137,40329,8330],{"class":4036},[137,40331,4053],{"class":157},[137,40333,40334,40336,40338,40341,40343],{"class":139,"line":302},[137,40335,23852],{"class":157},[137,40337,27],{"class":4036},[137,40339,40340],{"class":157},">Count: {count}\u003C\u002F",[137,40342,27],{"class":4036},[137,40344,4053],{"class":157},[137,40346,40347,40349,40351,40354,40356,40359,40361,40364,40367,40369,40371,40374,40376],{"class":139,"line":662},[137,40348,23852],{"class":157},[137,40350,8170],{"class":4036},[137,40352,40353],{"class":147}," onClick",[137,40355,253],{"class":143},[137,40357,40358],{"class":157},"{() ",[137,40360,222],{"class":143},[137,40362,40363],{"class":147}," setCount",[137,40365,40366],{"class":157},"(count ",[137,40368,182],{"class":143},[137,40370,8030],{"class":364},[137,40372,40373],{"class":157},")}>Increment\u003C\u002F",[137,40375,8170],{"class":4036},[137,40377,4053],{"class":157},[137,40379,40380,40382,40384],{"class":139,"line":667},[137,40381,9843],{"class":157},[137,40383,8330],{"class":4036},[137,40385,4053],{"class":157},[137,40387,40388],{"class":139,"line":786},[137,40389,11875],{"class":157},[137,40391,40392],{"class":139,"line":798},[137,40393,510],{"class":157},[137,40395,40396],{"class":139,"line":803},[137,40397,516],{"emptyLinePlaceholder":515},[137,40399,40400,40402,40404],{"class":139,"line":931},[137,40401,13456],{"class":143},[137,40403,21723],{"class":143},[137,40405,39325],{"class":157},[27,40407,40408,40409,40412],{},"For more complex state logic, React uses the ",[22,40410,40411],{},"useReducer"," hook. It is ideal when state updates involve multiple sub-values or actions.",[128,40414,40416],{"className":36884,"code":40415,"language":29196,"meta":133,"style":133},"import { useReducer } from \"react\";\n\nfunction reducer(state, action) {\n    switch (action.type) {\n        case \"increment\":\n            return { count: state.count + 1 };\n        case \"decrement\":\n            return { count: state.count - 1 };\n        default:\n            throw new Error();\n    }\n}\n\nfunction Counter() {\n    const [state, dispatch] = useReducer(reducer, { count: 0 });\n\n    return (\n        \u003Cdiv>\n            \u003Cp>Current count: {state.count}\u003C\u002Fp>\n            \u003Cbutton onClick={() => dispatch({ type: \"increment\" })}>Increment\u003C\u002Fbutton>\n            \u003Cbutton onClick={() => dispatch({ type: \"decrement\" })}>Decrement\u003C\u002Fbutton>\n        \u003C\u002Fdiv>\n    );\n}\n\nexport default Counter;\n",[22,40417,40418,40431,40435,40453,40461,40471,40484,40493,40505,40512,40523,40527,40531,40535,40544,40571,40575,40581,40589,40602,40632,40660,40668,40672,40676,40680],{"__ignoreMap":133},[137,40419,40420,40422,40425,40427,40429],{"class":139,"line":140},[137,40421,10287],{"class":143},[137,40423,40424],{"class":157}," { useReducer } ",[137,40426,10954],{"class":143},[137,40428,37226],{"class":284},[137,40430,3276],{"class":157},[137,40432,40433],{"class":139,"line":173},[137,40434,516],{"emptyLinePlaceholder":515},[137,40436,40437,40439,40442,40444,40447,40449,40451],{"class":139,"line":188},[137,40438,483],{"class":143},[137,40440,40441],{"class":147}," reducer",[137,40443,356],{"class":157},[137,40445,40446],{"class":161},"state",[137,40448,164],{"class":157},[137,40450,30194],{"class":161},[137,40452,170],{"class":157},[137,40454,40455,40458],{"class":139,"line":269},[137,40456,40457],{"class":143},"    switch",[137,40459,40460],{"class":157}," (action.type) {\n",[137,40462,40463,40466,40469],{"class":139,"line":278},[137,40464,40465],{"class":143},"        case",[137,40467,40468],{"class":284}," \"increment\"",[137,40470,36830],{"class":157},[137,40472,40473,40475,40478,40480,40482],{"class":139,"line":291},[137,40474,4683],{"class":143},[137,40476,40477],{"class":157}," { count: state.count ",[137,40479,182],{"class":143},[137,40481,8030],{"class":364},[137,40483,32107],{"class":157},[137,40485,40486,40488,40491],{"class":139,"line":297},[137,40487,40465],{"class":143},[137,40489,40490],{"class":284}," \"decrement\"",[137,40492,36830],{"class":157},[137,40494,40495,40497,40499,40501,40503],{"class":139,"line":302},[137,40496,4683],{"class":143},[137,40498,40477],{"class":157},[137,40500,8215],{"class":143},[137,40502,8030],{"class":364},[137,40504,32107],{"class":157},[137,40506,40507,40510],{"class":139,"line":662},[137,40508,40509],{"class":143},"        default",[137,40511,36830],{"class":157},[137,40513,40514,40516,40518,40521],{"class":139,"line":667},[137,40515,15739],{"class":143},[137,40517,1426],{"class":143},[137,40519,40520],{"class":147}," Error",[137,40522,924],{"class":157},[137,40524,40525],{"class":139,"line":786},[137,40526,294],{"class":157},[137,40528,40529],{"class":139,"line":798},[137,40530,510],{"class":157},[137,40532,40533],{"class":139,"line":803},[137,40534,516],{"emptyLinePlaceholder":515},[137,40536,40537,40539,40542],{"class":139,"line":931},[137,40538,483],{"class":143},[137,40540,40541],{"class":147}," Counter",[137,40543,275],{"class":157},[137,40545,40546,40548,40550,40552,40554,40557,40559,40561,40564,40567,40569],{"class":139,"line":1568},[137,40547,4177],{"class":143},[137,40549,22130],{"class":157},[137,40551,40446],{"class":364},[137,40553,164],{"class":157},[137,40555,40556],{"class":364},"dispatch",[137,40558,5796],{"class":157},[137,40560,253],{"class":143},[137,40562,40563],{"class":147}," useReducer",[137,40565,40566],{"class":157},"(reducer, { count: ",[137,40568,6044],{"class":364},[137,40570,4168],{"class":157},[137,40572,40573],{"class":139,"line":1573},[137,40574,516],{"emptyLinePlaceholder":515},[137,40576,40577,40579],{"class":139,"line":1578},[137,40578,176],{"class":143},[137,40580,30009],{"class":157},[137,40582,40583,40585,40587],{"class":139,"line":1588},[137,40584,9826],{"class":157},[137,40586,8330],{"class":4036},[137,40588,4053],{"class":157},[137,40590,40591,40593,40595,40598,40600],{"class":139,"line":1601},[137,40592,23852],{"class":157},[137,40594,27],{"class":4036},[137,40596,40597],{"class":157},">Current count: {state.count}\u003C\u002F",[137,40599,27],{"class":4036},[137,40601,4053],{"class":157},[137,40603,40604,40606,40608,40610,40612,40614,40616,40619,40622,40625,40628,40630],{"class":139,"line":3802},[137,40605,23852],{"class":157},[137,40607,8170],{"class":4036},[137,40609,40353],{"class":147},[137,40611,253],{"class":143},[137,40613,40358],{"class":157},[137,40615,222],{"class":143},[137,40617,40618],{"class":147}," dispatch",[137,40620,40621],{"class":157},"({ type: ",[137,40623,40624],{"class":284},"\"increment\"",[137,40626,40627],{"class":157}," })}>Increment\u003C\u002F",[137,40629,8170],{"class":4036},[137,40631,4053],{"class":157},[137,40633,40634,40636,40638,40640,40642,40644,40646,40648,40650,40653,40656,40658],{"class":139,"line":3808},[137,40635,23852],{"class":157},[137,40637,8170],{"class":4036},[137,40639,40353],{"class":147},[137,40641,253],{"class":143},[137,40643,40358],{"class":157},[137,40645,222],{"class":143},[137,40647,40618],{"class":147},[137,40649,40621],{"class":157},[137,40651,40652],{"class":284},"\"decrement\"",[137,40654,40655],{"class":157}," })}>Decrement\u003C\u002F",[137,40657,8170],{"class":4036},[137,40659,4053],{"class":157},[137,40661,40662,40664,40666],{"class":139,"line":3822},[137,40663,9843],{"class":157},[137,40665,8330],{"class":4036},[137,40667,4053],{"class":157},[137,40669,40670],{"class":139,"line":3827},[137,40671,11875],{"class":157},[137,40673,40674],{"class":139,"line":3832},[137,40675,510],{"class":157},[137,40677,40678],{"class":139,"line":3840},[137,40679,516],{"emptyLinePlaceholder":515},[137,40681,40682,40684,40686],{"class":139,"line":3846},[137,40683,13456],{"class":143},[137,40685,21723],{"class":143},[137,40687,40688],{"class":157}," Counter;\n",[27,40690,40691,40692,40695],{},"To derive state based on other variables, React uses memoisation with ",[22,40693,40694],{},"useMemo",". This hook optimises performance by storing computed values and preventing unnecessary recalculations when dependencies haven't changed.",[128,40697,40699],{"className":36884,"code":40698,"language":29196,"meta":133,"style":133},"import { useState, useMemo } from \"react\";\n\nfunction DoubleCounter() {\n    const [count, setCount] = useState(0);\n\n    \u002F\u002F Derived state\n    const doubleCount = useMemo(() => count * 2, [count]);\n\n    return (\n        \u003Cdiv>\n            \u003Cp>Count: {count}\u003C\u002Fp>\n            \u003Cp>Double Count: {doubleCount}\u003C\u002Fp>\n            \u003Cbutton onClick={() => setCount(count + 1)}>Increment\u003C\u002Fbutton>\n        \u003C\u002Fdiv>\n    );\n}\n\nexport default DoubleCounter;\n",[22,40700,40701,40714,40718,40727,40751,40755,40760,40787,40791,40797,40805,40817,40830,40858,40866,40870,40874,40878],{"__ignoreMap":133},[137,40702,40703,40705,40708,40710,40712],{"class":139,"line":140},[137,40704,10287],{"class":143},[137,40706,40707],{"class":157}," { useState, useMemo } ",[137,40709,10954],{"class":143},[137,40711,37226],{"class":284},[137,40713,3276],{"class":157},[137,40715,40716],{"class":139,"line":173},[137,40717,516],{"emptyLinePlaceholder":515},[137,40719,40720,40722,40725],{"class":139,"line":188},[137,40721,483],{"class":143},[137,40723,40724],{"class":147}," DoubleCounter",[137,40726,275],{"class":157},[137,40728,40729,40731,40733,40735,40737,40739,40741,40743,40745,40747,40749],{"class":139,"line":269},[137,40730,4177],{"class":143},[137,40732,22130],{"class":157},[137,40734,40296],{"class":364},[137,40736,164],{"class":157},[137,40738,40301],{"class":364},[137,40740,5796],{"class":157},[137,40742,253],{"class":143},[137,40744,37266],{"class":147},[137,40746,356],{"class":157},[137,40748,6044],{"class":364},[137,40750,1502],{"class":157},[137,40752,40753],{"class":139,"line":278},[137,40754,516],{"emptyLinePlaceholder":515},[137,40756,40757],{"class":139,"line":291},[137,40758,40759],{"class":308},"    \u002F\u002F Derived state\n",[137,40761,40762,40764,40767,40769,40772,40774,40776,40779,40781,40784],{"class":139,"line":297},[137,40763,4177],{"class":143},[137,40765,40766],{"class":364}," doubleCount",[137,40768,151],{"class":143},[137,40770,40771],{"class":147}," useMemo",[137,40773,3193],{"class":157},[137,40775,222],{"class":143},[137,40777,40778],{"class":157}," count ",[137,40780,7672],{"class":143},[137,40782,40783],{"class":364}," 2",[137,40785,40786],{"class":157},", [count]);\n",[137,40788,40789],{"class":139,"line":302},[137,40790,516],{"emptyLinePlaceholder":515},[137,40792,40793,40795],{"class":139,"line":662},[137,40794,176],{"class":143},[137,40796,30009],{"class":157},[137,40798,40799,40801,40803],{"class":139,"line":667},[137,40800,9826],{"class":157},[137,40802,8330],{"class":4036},[137,40804,4053],{"class":157},[137,40806,40807,40809,40811,40813,40815],{"class":139,"line":786},[137,40808,23852],{"class":157},[137,40810,27],{"class":4036},[137,40812,40340],{"class":157},[137,40814,27],{"class":4036},[137,40816,4053],{"class":157},[137,40818,40819,40821,40823,40826,40828],{"class":139,"line":798},[137,40820,23852],{"class":157},[137,40822,27],{"class":4036},[137,40824,40825],{"class":157},">Double Count: {doubleCount}\u003C\u002F",[137,40827,27],{"class":4036},[137,40829,4053],{"class":157},[137,40831,40832,40834,40836,40838,40840,40842,40844,40846,40848,40850,40852,40854,40856],{"class":139,"line":803},[137,40833,23852],{"class":157},[137,40835,8170],{"class":4036},[137,40837,40353],{"class":147},[137,40839,253],{"class":143},[137,40841,40358],{"class":157},[137,40843,222],{"class":143},[137,40845,40363],{"class":147},[137,40847,40366],{"class":157},[137,40849,182],{"class":143},[137,40851,8030],{"class":364},[137,40853,40373],{"class":157},[137,40855,8170],{"class":4036},[137,40857,4053],{"class":157},[137,40859,40860,40862,40864],{"class":139,"line":931},[137,40861,9843],{"class":157},[137,40863,8330],{"class":4036},[137,40865,4053],{"class":157},[137,40867,40868],{"class":139,"line":1568},[137,40869,11875],{"class":157},[137,40871,40872],{"class":139,"line":1573},[137,40873,510],{"class":157},[137,40875,40876],{"class":139,"line":1578},[137,40877,516],{"emptyLinePlaceholder":515},[137,40879,40880,40882,40884],{"class":139,"line":1588},[137,40881,13456],{"class":143},[137,40883,21723],{"class":143},[137,40885,40886],{"class":157}," DoubleCounter;\n",[123,40888,26585],{"id":40889},"vue-3",[27,40891,40892,40893,40895],{},"In Vue, you use ",[22,40894,27815],{}," to manage state reactively:",[128,40897,40899],{"className":4024,"code":40898,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import { ref } from \"vue\";\n\n    const count = ref(0);\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv>\n        \u003Cp>Count: {{ count }}\u003C\u002Fp>\n        \u003Cbutton @click=\"count++\">Increment\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,40900,40901,40911,40924,40928,40945,40953,40957,40965,40973,40986,41007,41015],{"__ignoreMap":133},[137,40902,40903,40905,40907,40909],{"class":139,"line":140},[137,40904,4033],{"class":157},[137,40906,4037],{"class":4036},[137,40908,9642],{"class":147},[137,40910,4053],{"class":157},[137,40912,40913,40915,40918,40920,40922],{"class":139,"line":173},[137,40914,37677],{"class":143},[137,40916,40917],{"class":157}," { ref } ",[137,40919,10954],{"class":143},[137,40921,11091],{"class":284},[137,40923,3276],{"class":157},[137,40925,40926],{"class":139,"line":188},[137,40927,516],{"emptyLinePlaceholder":515},[137,40929,40930,40932,40935,40937,40939,40941,40943],{"class":139,"line":269},[137,40931,4177],{"class":143},[137,40933,40934],{"class":364}," count",[137,40936,151],{"class":143},[137,40938,10468],{"class":147},[137,40940,356],{"class":157},[137,40942,6044],{"class":364},[137,40944,1502],{"class":157},[137,40946,40947,40949,40951],{"class":139,"line":278},[137,40948,4083],{"class":157},[137,40950,4037],{"class":4036},[137,40952,4053],{"class":157},[137,40954,40955],{"class":139,"line":291},[137,40956,516],{"emptyLinePlaceholder":515},[137,40958,40959,40961,40963],{"class":139,"line":297},[137,40960,4033],{"class":157},[137,40962,7821],{"class":4036},[137,40964,4053],{"class":157},[137,40966,40967,40969,40971],{"class":139,"line":302},[137,40968,4072],{"class":157},[137,40970,8330],{"class":4036},[137,40972,4053],{"class":157},[137,40974,40975,40977,40979,40982,40984],{"class":139,"line":662},[137,40976,9826],{"class":157},[137,40978,27],{"class":4036},[137,40980,40981],{"class":157},">Count: {{ count }}\u003C\u002F",[137,40983,27],{"class":4036},[137,40985,4053],{"class":157},[137,40987,40988,40990,40992,40995,40997,41000,41003,41005],{"class":139,"line":667},[137,40989,9826],{"class":157},[137,40991,8170],{"class":4036},[137,40993,40994],{"class":147}," @click",[137,40996,253],{"class":157},[137,40998,40999],{"class":284},"\"count++\"",[137,41001,41002],{"class":157},">Increment\u003C\u002F",[137,41004,8170],{"class":4036},[137,41006,4053],{"class":157},[137,41008,41009,41011,41013],{"class":139,"line":786},[137,41010,8374],{"class":157},[137,41012,8330],{"class":4036},[137,41014,4053],{"class":157},[137,41016,41017,41019,41021],{"class":139,"line":798},[137,41018,4083],{"class":157},[137,41020,7821],{"class":4036},[137,41022,4053],{"class":157},[27,41024,41025,41026,41029],{},"Alternatively you can use ",[22,41027,41028],{},"reactive"," function to creates a reactive object, which is useful for managing more complex state (e.g., multiple related properties).",[128,41031,41033],{"className":4024,"code":41032,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import { reactive } from \"vue\";\n\n    const state = reactive({\n        count: 0,\n        message: \"Hello, Vue 3!\",\n    });\n\n    const increment = () => {\n        state.count++;\n    };\n\n    const updateMessage = () => {\n        state.message = \"You updated the message!\";\n    };\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv>\n        \u003Cp>Count: {{ state.count }}\u003C\u002Fp>\n        \u003Cp>Message: {{ state.message }}\u003C\u002Fp>\n        \u003Cbutton @click=\"increment\">Increment\u003C\u002Fbutton>\n        \u003Cbutton @click=\"updateMessage\">Change Message\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,41034,41035,41045,41058,41062,41076,41085,41094,41098,41102,41117,41126,41130,41134,41149,41161,41165,41173,41177,41185,41193,41206,41219,41237,41257,41265],{"__ignoreMap":133},[137,41036,41037,41039,41041,41043],{"class":139,"line":140},[137,41038,4033],{"class":157},[137,41040,4037],{"class":4036},[137,41042,9642],{"class":147},[137,41044,4053],{"class":157},[137,41046,41047,41049,41052,41054,41056],{"class":139,"line":173},[137,41048,37677],{"class":143},[137,41050,41051],{"class":157}," { reactive } ",[137,41053,10954],{"class":143},[137,41055,11091],{"class":284},[137,41057,3276],{"class":157},[137,41059,41060],{"class":139,"line":188},[137,41061,516],{"emptyLinePlaceholder":515},[137,41063,41064,41066,41069,41071,41074],{"class":139,"line":269},[137,41065,4177],{"class":143},[137,41067,41068],{"class":364}," state",[137,41070,151],{"class":143},[137,41072,41073],{"class":147}," reactive",[137,41075,3175],{"class":157},[137,41077,41078,41081,41083],{"class":139,"line":278},[137,41079,41080],{"class":157},"        count: ",[137,41082,6044],{"class":364},[137,41084,1961],{"class":157},[137,41086,41087,41089,41092],{"class":139,"line":291},[137,41088,14798],{"class":157},[137,41090,41091],{"class":284},"\"Hello, Vue 3!\"",[137,41093,1961],{"class":157},[137,41095,41096],{"class":139,"line":297},[137,41097,2832],{"class":157},[137,41099,41100],{"class":139,"line":302},[137,41101,516],{"emptyLinePlaceholder":515},[137,41103,41104,41106,41109,41111,41113,41115],{"class":139,"line":662},[137,41105,4177],{"class":143},[137,41107,41108],{"class":147}," increment",[137,41110,151],{"class":143},[137,41112,1484],{"class":157},[137,41114,222],{"class":143},[137,41116,256],{"class":157},[137,41118,41119,41122,41124],{"class":139,"line":667},[137,41120,41121],{"class":157},"        state.count",[137,41123,12683],{"class":143},[137,41125,3276],{"class":157},[137,41127,41128],{"class":139,"line":786},[137,41129,1892],{"class":157},[137,41131,41132],{"class":139,"line":798},[137,41133,516],{"emptyLinePlaceholder":515},[137,41135,41136,41138,41141,41143,41145,41147],{"class":139,"line":803},[137,41137,4177],{"class":143},[137,41139,41140],{"class":147}," updateMessage",[137,41142,151],{"class":143},[137,41144,1484],{"class":157},[137,41146,222],{"class":143},[137,41148,256],{"class":157},[137,41150,41151,41154,41156,41159],{"class":139,"line":931},[137,41152,41153],{"class":157},"        state.message ",[137,41155,253],{"class":143},[137,41157,41158],{"class":284}," \"You updated the message!\"",[137,41160,3276],{"class":157},[137,41162,41163],{"class":139,"line":1568},[137,41164,1892],{"class":157},[137,41166,41167,41169,41171],{"class":139,"line":1573},[137,41168,4083],{"class":157},[137,41170,4037],{"class":4036},[137,41172,4053],{"class":157},[137,41174,41175],{"class":139,"line":1578},[137,41176,516],{"emptyLinePlaceholder":515},[137,41178,41179,41181,41183],{"class":139,"line":1588},[137,41180,4033],{"class":157},[137,41182,7821],{"class":4036},[137,41184,4053],{"class":157},[137,41186,41187,41189,41191],{"class":139,"line":1601},[137,41188,4072],{"class":157},[137,41190,8330],{"class":4036},[137,41192,4053],{"class":157},[137,41194,41195,41197,41199,41202,41204],{"class":139,"line":3802},[137,41196,9826],{"class":157},[137,41198,27],{"class":4036},[137,41200,41201],{"class":157},">Count: {{ state.count }}\u003C\u002F",[137,41203,27],{"class":4036},[137,41205,4053],{"class":157},[137,41207,41208,41210,41212,41215,41217],{"class":139,"line":3808},[137,41209,9826],{"class":157},[137,41211,27],{"class":4036},[137,41213,41214],{"class":157},">Message: {{ state.message }}\u003C\u002F",[137,41216,27],{"class":4036},[137,41218,4053],{"class":157},[137,41220,41221,41223,41225,41227,41229,41231,41233,41235],{"class":139,"line":3822},[137,41222,9826],{"class":157},[137,41224,8170],{"class":4036},[137,41226,40994],{"class":147},[137,41228,253],{"class":157},[137,41230,40624],{"class":284},[137,41232,41002],{"class":157},[137,41234,8170],{"class":4036},[137,41236,4053],{"class":157},[137,41238,41239,41241,41243,41245,41247,41250,41253,41255],{"class":139,"line":3827},[137,41240,9826],{"class":157},[137,41242,8170],{"class":4036},[137,41244,40994],{"class":147},[137,41246,253],{"class":157},[137,41248,41249],{"class":284},"\"updateMessage\"",[137,41251,41252],{"class":157},">Change Message\u003C\u002F",[137,41254,8170],{"class":4036},[137,41256,4053],{"class":157},[137,41258,41259,41261,41263],{"class":139,"line":3832},[137,41260,8374],{"class":157},[137,41262,8330],{"class":4036},[137,41264,4053],{"class":157},[137,41266,41267,41269,41271],{"class":139,"line":3840},[137,41268,4083],{"class":157},[137,41270,7821],{"class":4036},[137,41272,4053],{"class":157},[27,41274,41275,41276,41278],{},"In Vue you can derive state with ",[22,41277,27292],{}," function, which automatically updates when its dependencies change.",[128,41280,41282],{"className":4024,"code":41281,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import { ref, computed } from \"vue\";\n\n    const count = ref(0);\n\n    const doubleCount = computed(() => count.value * 2);\n\n    const increment = () => {\n        count.value++;\n    };\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv>\n        \u003Cp>Count: {{ count }}\u003C\u002Fp>\n        \u003Cp>Double Count: {{ doubleCount }}\u003C\u002Fp>\n        \u003Cbutton @click=\"increment\">Increment\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,41283,41284,41294,41307,41311,41327,41331,41354,41358,41372,41381,41385,41393,41397,41405,41413,41425,41438,41456,41464],{"__ignoreMap":133},[137,41285,41286,41288,41290,41292],{"class":139,"line":140},[137,41287,4033],{"class":157},[137,41289,4037],{"class":4036},[137,41291,9642],{"class":147},[137,41293,4053],{"class":157},[137,41295,41296,41298,41301,41303,41305],{"class":139,"line":173},[137,41297,37677],{"class":143},[137,41299,41300],{"class":157}," { ref, computed } ",[137,41302,10954],{"class":143},[137,41304,11091],{"class":284},[137,41306,3276],{"class":157},[137,41308,41309],{"class":139,"line":188},[137,41310,516],{"emptyLinePlaceholder":515},[137,41312,41313,41315,41317,41319,41321,41323,41325],{"class":139,"line":269},[137,41314,4177],{"class":143},[137,41316,40934],{"class":364},[137,41318,151],{"class":143},[137,41320,10468],{"class":147},[137,41322,356],{"class":157},[137,41324,6044],{"class":364},[137,41326,1502],{"class":157},[137,41328,41329],{"class":139,"line":278},[137,41330,516],{"emptyLinePlaceholder":515},[137,41332,41333,41335,41337,41339,41341,41343,41345,41348,41350,41352],{"class":139,"line":291},[137,41334,4177],{"class":143},[137,41336,40766],{"class":364},[137,41338,151],{"class":143},[137,41340,26944],{"class":147},[137,41342,3193],{"class":157},[137,41344,222],{"class":143},[137,41346,41347],{"class":157}," count.value ",[137,41349,7672],{"class":143},[137,41351,40783],{"class":364},[137,41353,1502],{"class":157},[137,41355,41356],{"class":139,"line":297},[137,41357,516],{"emptyLinePlaceholder":515},[137,41359,41360,41362,41364,41366,41368,41370],{"class":139,"line":302},[137,41361,4177],{"class":143},[137,41363,41108],{"class":147},[137,41365,151],{"class":143},[137,41367,1484],{"class":157},[137,41369,222],{"class":143},[137,41371,256],{"class":157},[137,41373,41374,41377,41379],{"class":139,"line":662},[137,41375,41376],{"class":157},"        count.value",[137,41378,12683],{"class":143},[137,41380,3276],{"class":157},[137,41382,41383],{"class":139,"line":667},[137,41384,1892],{"class":157},[137,41386,41387,41389,41391],{"class":139,"line":786},[137,41388,4083],{"class":157},[137,41390,4037],{"class":4036},[137,41392,4053],{"class":157},[137,41394,41395],{"class":139,"line":798},[137,41396,516],{"emptyLinePlaceholder":515},[137,41398,41399,41401,41403],{"class":139,"line":803},[137,41400,4033],{"class":157},[137,41402,7821],{"class":4036},[137,41404,4053],{"class":157},[137,41406,41407,41409,41411],{"class":139,"line":931},[137,41408,4072],{"class":157},[137,41410,8330],{"class":4036},[137,41412,4053],{"class":157},[137,41414,41415,41417,41419,41421,41423],{"class":139,"line":1568},[137,41416,9826],{"class":157},[137,41418,27],{"class":4036},[137,41420,40981],{"class":157},[137,41422,27],{"class":4036},[137,41424,4053],{"class":157},[137,41426,41427,41429,41431,41434,41436],{"class":139,"line":1573},[137,41428,9826],{"class":157},[137,41430,27],{"class":4036},[137,41432,41433],{"class":157},">Double Count: {{ doubleCount }}\u003C\u002F",[137,41435,27],{"class":4036},[137,41437,4053],{"class":157},[137,41439,41440,41442,41444,41446,41448,41450,41452,41454],{"class":139,"line":1578},[137,41441,9826],{"class":157},[137,41443,8170],{"class":4036},[137,41445,40994],{"class":147},[137,41447,253],{"class":157},[137,41449,40624],{"class":284},[137,41451,41002],{"class":157},[137,41453,8170],{"class":4036},[137,41455,4053],{"class":157},[137,41457,41458,41460,41462],{"class":139,"line":1588},[137,41459,8374],{"class":157},[137,41461,8330],{"class":4036},[137,41463,4053],{"class":157},[137,41465,41466,41468,41470],{"class":139,"line":1601},[137,41467,4083],{"class":157},[137,41469,7821],{"class":4036},[137,41471,4053],{"class":157},[27,41473,41474],{},[42,41475,39387],{},[1003,41477,41478,41483,41489,41494,41505,41515],{},[1006,41479,39860,41480,41482],{},[22,41481,27815],{}," for creating reactive variables.",[1006,41484,41485,41486,41488],{},"For objects with multiple properties, Vue's ",[22,41487,41028],{}," function is more appropriate.",[1006,41490,9726,41491,41493],{},[22,41492,27292],{}," properties to create derived or computed state.",[1006,41495,41496,41497,41499,41500,41502,41503,1017],{},"When working in the ",[22,41498,4037],{}," section, always access ",[22,41501,27815],{}," values with ",[22,41504,27330],{},[1006,41506,41507,41508,41511,41512,41514],{},"Vue uses double curly braces ",[22,41509,41510],{},"{{ }}"," for data binding in templates. Unlike in the ",[22,41513,4037],{}," section, you don't need to manually unwrap reactive values in templates.",[1006,41516,41517,41518,41521],{},"Vue uses a Proxy-based reactivity system that offers several advantages over React. Vue uses Proxies to observe changes in reactive objects and automatically track dependencies, allowing it to detect mutations directly. React, in contrast, relies on explicit updates (via ",[22,41519,41520],{},"setState"," or similar) and immutable data structures to trigger reactivity, avoiding deep observation of objects.",[104,41523,41525,41526],{"id":41524},"two-way-binding-with-v-model","Two-Way Binding with ",[22,41527,41528],{},"v-model",[27,41530,39595,41531,41533],{},[22,41532,41528],{}," simplifies two-way data binding between form inputs and reactive variables. This feature is intuitive and eliminates the need for explicit event listeners and state updates, as commonly done in React.",[123,41535,36801],{"id":41536},"react-4",[27,41538,41539],{},"React does not have built-in two-way binding. Instead, developers manually link the state to the input’s value and use event handlers to update the state:",[128,41541,41543],{"className":36884,"code":41542,"language":29196,"meta":133,"style":133},"import { useState } from \"react\";\n\nfunction TwoWayBindingExample() {\n    const [inputValue, setInputValue] = useState(\"\");\n\n    const handleChange = (event) => {\n        setInputValue(event.target.value);\n    };\n\n    return (\n        \u003Cdiv>\n            \u003Cinput value={inputValue} onChange={handleChange} placeholder=\"Type something...\" \u002F>\n            \u003Cp>You typed: {inputValue}\u003C\u002Fp>\n        \u003C\u002Fdiv>\n    );\n}\n\nexport default TwoWayBindingExample;\n",[22,41544,41545,41557,41561,41570,41596,41600,41619,41627,41631,41635,41641,41649,41680,41693,41701,41705,41709,41713],{"__ignoreMap":133},[137,41546,41547,41549,41551,41553,41555],{"class":139,"line":140},[137,41548,10287],{"class":143},[137,41550,40269],{"class":157},[137,41552,10954],{"class":143},[137,41554,37226],{"class":284},[137,41556,3276],{"class":157},[137,41558,41559],{"class":139,"line":173},[137,41560,516],{"emptyLinePlaceholder":515},[137,41562,41563,41565,41568],{"class":139,"line":188},[137,41564,483],{"class":143},[137,41566,41567],{"class":147}," TwoWayBindingExample",[137,41569,275],{"class":157},[137,41571,41572,41574,41576,41579,41581,41584,41586,41588,41590,41592,41594],{"class":139,"line":269},[137,41573,4177],{"class":143},[137,41575,22130],{"class":157},[137,41577,41578],{"class":364},"inputValue",[137,41580,164],{"class":157},[137,41582,41583],{"class":364},"setInputValue",[137,41585,5796],{"class":157},[137,41587,253],{"class":143},[137,41589,37266],{"class":147},[137,41591,356],{"class":157},[137,41593,4535],{"class":284},[137,41595,1502],{"class":157},[137,41597,41598],{"class":139,"line":278},[137,41599,516],{"emptyLinePlaceholder":515},[137,41601,41602,41604,41607,41609,41611,41613,41615,41617],{"class":139,"line":291},[137,41603,4177],{"class":143},[137,41605,41606],{"class":147}," handleChange",[137,41608,151],{"class":143},[137,41610,158],{"class":157},[137,41612,24689],{"class":161},[137,41614,219],{"class":157},[137,41616,222],{"class":143},[137,41618,256],{"class":157},[137,41620,41621,41624],{"class":139,"line":297},[137,41622,41623],{"class":147},"        setInputValue",[137,41625,41626],{"class":157},"(event.target.value);\n",[137,41628,41629],{"class":139,"line":302},[137,41630,1892],{"class":157},[137,41632,41633],{"class":139,"line":662},[137,41634,516],{"emptyLinePlaceholder":515},[137,41636,41637,41639],{"class":139,"line":667},[137,41638,176],{"class":143},[137,41640,30009],{"class":157},[137,41642,41643,41645,41647],{"class":139,"line":786},[137,41644,9826],{"class":157},[137,41646,8330],{"class":4036},[137,41648,4053],{"class":157},[137,41650,41651,41653,41655,41658,41660,41663,41665,41667,41670,41673,41675,41678],{"class":139,"line":798},[137,41652,23852],{"class":157},[137,41654,8520],{"class":4036},[137,41656,41657],{"class":147}," value",[137,41659,253],{"class":143},[137,41661,41662],{"class":157},"{inputValue} ",[137,41664,35518],{"class":147},[137,41666,253],{"class":143},[137,41668,41669],{"class":157},"{handleChange} ",[137,41671,41672],{"class":147},"placeholder",[137,41674,253],{"class":143},[137,41676,41677],{"class":284},"\"Type something...\"",[137,41679,4078],{"class":157},[137,41681,41682,41684,41686,41689,41691],{"class":139,"line":803},[137,41683,23852],{"class":157},[137,41685,27],{"class":4036},[137,41687,41688],{"class":157},">You typed: {inputValue}\u003C\u002F",[137,41690,27],{"class":4036},[137,41692,4053],{"class":157},[137,41694,41695,41697,41699],{"class":139,"line":931},[137,41696,9843],{"class":157},[137,41698,8330],{"class":4036},[137,41700,4053],{"class":157},[137,41702,41703],{"class":139,"line":1568},[137,41704,11875],{"class":157},[137,41706,41707],{"class":139,"line":1573},[137,41708,510],{"class":157},[137,41710,41711],{"class":139,"line":1578},[137,41712,516],{"emptyLinePlaceholder":515},[137,41714,41715,41717,41719],{"class":139,"line":1588},[137,41716,13456],{"class":143},[137,41718,21723],{"class":143},[137,41720,41721],{"class":157}," TwoWayBindingExample;\n",[27,41723,41724,41725,41727],{},"In Vue, using ",[22,41726,41528],{}," is straightforward. It automatically binds the input’s value to a reactive variable and updates the variable whenever the input changes.",[123,41729,26585],{"id":41730},"vue-4",[128,41732,41734],{"className":4024,"code":41733,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import { ref } from \"vue\";\n\n    const inputValue = ref(\"\");\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv>\n        \u003Cinput v-model=\"inputValue\" placeholder=\"Type something...\" \u002F>\n        \u003Cp>You typed: {{ inputValue }}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,41735,41736,41746,41758,41762,41779,41787,41791,41799,41807,41829,41842,41850],{"__ignoreMap":133},[137,41737,41738,41740,41742,41744],{"class":139,"line":140},[137,41739,4033],{"class":157},[137,41741,4037],{"class":4036},[137,41743,9642],{"class":147},[137,41745,4053],{"class":157},[137,41747,41748,41750,41752,41754,41756],{"class":139,"line":173},[137,41749,37677],{"class":143},[137,41751,40917],{"class":157},[137,41753,10954],{"class":143},[137,41755,11091],{"class":284},[137,41757,3276],{"class":157},[137,41759,41760],{"class":139,"line":188},[137,41761,516],{"emptyLinePlaceholder":515},[137,41763,41764,41766,41769,41771,41773,41775,41777],{"class":139,"line":269},[137,41765,4177],{"class":143},[137,41767,41768],{"class":364}," inputValue",[137,41770,151],{"class":143},[137,41772,10468],{"class":147},[137,41774,356],{"class":157},[137,41776,4535],{"class":284},[137,41778,1502],{"class":157},[137,41780,41781,41783,41785],{"class":139,"line":278},[137,41782,4083],{"class":157},[137,41784,4037],{"class":4036},[137,41786,4053],{"class":157},[137,41788,41789],{"class":139,"line":291},[137,41790,516],{"emptyLinePlaceholder":515},[137,41792,41793,41795,41797],{"class":139,"line":297},[137,41794,4033],{"class":157},[137,41796,7821],{"class":4036},[137,41798,4053],{"class":157},[137,41800,41801,41803,41805],{"class":139,"line":302},[137,41802,4072],{"class":157},[137,41804,8330],{"class":4036},[137,41806,4053],{"class":157},[137,41808,41809,41811,41813,41816,41818,41821,41823,41825,41827],{"class":139,"line":662},[137,41810,9826],{"class":157},[137,41812,8520],{"class":4036},[137,41814,41815],{"class":147}," v-model",[137,41817,253],{"class":157},[137,41819,41820],{"class":284},"\"inputValue\"",[137,41822,36233],{"class":147},[137,41824,253],{"class":157},[137,41826,41677],{"class":284},[137,41828,4078],{"class":157},[137,41830,41831,41833,41835,41838,41840],{"class":139,"line":667},[137,41832,9826],{"class":157},[137,41834,27],{"class":4036},[137,41836,41837],{"class":157},">You typed: {{ inputValue }}\u003C\u002F",[137,41839,27],{"class":4036},[137,41841,4053],{"class":157},[137,41843,41844,41846,41848],{"class":139,"line":786},[137,41845,8374],{"class":157},[137,41847,8330],{"class":4036},[137,41849,4053],{"class":157},[137,41851,41852,41854,41856],{"class":139,"line":798},[137,41853,4083],{"class":157},[137,41855,7821],{"class":4036},[137,41857,4053],{"class":157},[27,41859,41860],{},[42,41861,41862],{},"Key Differences",[1003,41864,41865],{},[1006,41866,41867,41868,41870],{},"Vue offers cleaner syntax with ",[22,41869,41528],{},", while React needs manual event handling",[104,41872,41874],{"id":41873},"lifecycle-hooks","Lifecycle Hooks",[123,41876,36801],{"id":41877},"react-5",[27,41879,41880,41881,41884],{},"React’s lifecycle hooks like ",[22,41882,41883],{},"useEffect"," handle mount and unmount logic:",[128,41886,41888],{"className":36884,"code":41887,"language":29196,"meta":133,"style":133},"import { useEffect } from \"react\";\n\nfunction ExampleComponent() {\n    useEffect(() => {\n        console.log(\"Component mounted\");\n\n        return () => {\n            console.log(\"Component unmounted\");\n        };\n    }, []);\n\n    return \u003Cdiv>Lifecycle example\u003C\u002Fdiv>;\n}\n\nexport default ExampleComponent;\n",[22,41889,41890,41903,41907,41915,41925,41938,41942,41952,41965,41969,41973,41977,41992,41996,42000],{"__ignoreMap":133},[137,41891,41892,41894,41897,41899,41901],{"class":139,"line":140},[137,41893,10287],{"class":143},[137,41895,41896],{"class":157}," { useEffect } ",[137,41898,10954],{"class":143},[137,41900,37226],{"class":284},[137,41902,3276],{"class":157},[137,41904,41905],{"class":139,"line":173},[137,41906,516],{"emptyLinePlaceholder":515},[137,41908,41909,41911,41913],{"class":139,"line":188},[137,41910,483],{"class":143},[137,41912,39291],{"class":147},[137,41914,275],{"class":157},[137,41916,41917,41919,41921,41923],{"class":139,"line":269},[137,41918,37281],{"class":147},[137,41920,3193],{"class":157},[137,41922,222],{"class":143},[137,41924,256],{"class":157},[137,41926,41927,41929,41931,41933,41936],{"class":139,"line":278},[137,41928,350],{"class":157},[137,41930,353],{"class":147},[137,41932,356],{"class":157},[137,41934,41935],{"class":284},"\"Component mounted\"",[137,41937,1502],{"class":157},[137,41939,41940],{"class":139,"line":291},[137,41941,516],{"emptyLinePlaceholder":515},[137,41943,41944,41946,41948,41950],{"class":139,"line":297},[137,41945,5472],{"class":143},[137,41947,1484],{"class":157},[137,41949,222],{"class":143},[137,41951,256],{"class":157},[137,41953,41954,41956,41958,41960,41963],{"class":139,"line":302},[137,41955,1493],{"class":157},[137,41957,353],{"class":147},[137,41959,356],{"class":157},[137,41961,41962],{"class":284},"\"Component unmounted\"",[137,41964,1502],{"class":157},[137,41966,41967],{"class":139,"line":662},[137,41968,1507],{"class":157},[137,41970,41971],{"class":139,"line":667},[137,41972,37457],{"class":157},[137,41974,41975],{"class":139,"line":786},[137,41976,516],{"emptyLinePlaceholder":515},[137,41978,41979,41981,41983,41985,41988,41990],{"class":139,"line":798},[137,41980,176],{"class":143},[137,41982,29304],{"class":157},[137,41984,8330],{"class":4036},[137,41986,41987],{"class":157},">Lifecycle example\u003C\u002F",[137,41989,8330],{"class":4036},[137,41991,29320],{"class":157},[137,41993,41994],{"class":139,"line":803},[137,41995,510],{"class":157},[137,41997,41998],{"class":139,"line":931},[137,41999,516],{"emptyLinePlaceholder":515},[137,42001,42002,42004,42006],{"class":139,"line":1568},[137,42003,13456],{"class":143},[137,42005,21723],{"class":143},[137,42007,39325],{"class":157},[123,42009,26585],{"id":42010},"vue-5",[27,42012,42013,42014,114,42016,894],{},"Vue provides lifecycle hooks like ",[22,42015,10538],{},[22,42017,42018],{},"onUnmounted",[128,42020,42022],{"className":4024,"code":42021,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import { onMounted, onUnmounted } from \"vue\";\n\n    onMounted(() => {\n        console.log(\"Component mounted\");\n    });\n\n    onUnmounted(() => {\n        console.log(\"Component unmounted\");\n    });\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv>Lifecycle example\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,42023,42024,42034,42047,42051,42061,42073,42077,42081,42092,42104,42108,42116,42120,42128,42140],{"__ignoreMap":133},[137,42025,42026,42028,42030,42032],{"class":139,"line":140},[137,42027,4033],{"class":157},[137,42029,4037],{"class":4036},[137,42031,9642],{"class":147},[137,42033,4053],{"class":157},[137,42035,42036,42038,42041,42043,42045],{"class":139,"line":173},[137,42037,37677],{"class":143},[137,42039,42040],{"class":157}," { onMounted, onUnmounted } ",[137,42042,10954],{"class":143},[137,42044,11091],{"class":284},[137,42046,3276],{"class":157},[137,42048,42049],{"class":139,"line":188},[137,42050,516],{"emptyLinePlaceholder":515},[137,42052,42053,42055,42057,42059],{"class":139,"line":269},[137,42054,34654],{"class":147},[137,42056,3193],{"class":157},[137,42058,222],{"class":143},[137,42060,256],{"class":157},[137,42062,42063,42065,42067,42069,42071],{"class":139,"line":278},[137,42064,350],{"class":157},[137,42066,353],{"class":147},[137,42068,356],{"class":157},[137,42070,41935],{"class":284},[137,42072,1502],{"class":157},[137,42074,42075],{"class":139,"line":291},[137,42076,2832],{"class":157},[137,42078,42079],{"class":139,"line":297},[137,42080,516],{"emptyLinePlaceholder":515},[137,42082,42083,42086,42088,42090],{"class":139,"line":302},[137,42084,42085],{"class":147},"    onUnmounted",[137,42087,3193],{"class":157},[137,42089,222],{"class":143},[137,42091,256],{"class":157},[137,42093,42094,42096,42098,42100,42102],{"class":139,"line":662},[137,42095,350],{"class":157},[137,42097,353],{"class":147},[137,42099,356],{"class":157},[137,42101,41962],{"class":284},[137,42103,1502],{"class":157},[137,42105,42106],{"class":139,"line":667},[137,42107,2832],{"class":157},[137,42109,42110,42112,42114],{"class":139,"line":786},[137,42111,4083],{"class":157},[137,42113,4037],{"class":4036},[137,42115,4053],{"class":157},[137,42117,42118],{"class":139,"line":798},[137,42119,516],{"emptyLinePlaceholder":515},[137,42121,42122,42124,42126],{"class":139,"line":803},[137,42123,4033],{"class":157},[137,42125,7821],{"class":4036},[137,42127,4053],{"class":157},[137,42129,42130,42132,42134,42136,42138],{"class":139,"line":931},[137,42131,4072],{"class":157},[137,42133,8330],{"class":4036},[137,42135,41987],{"class":157},[137,42137,8330],{"class":4036},[137,42139,4053],{"class":157},[137,42141,42142,42144,42146],{"class":139,"line":1568},[137,42143,4083],{"class":157},[137,42145,7821],{"class":4036},[137,42147,4053],{"class":157},[27,42149,42150],{},[42,42151,39387],{},[1003,42153,42154],{},[1006,42155,42156,42157,114,42159,42161],{},"Vue uses separate lifecycle hooks such as ",[22,42158,10538],{},[22,42160,42018],{}," to handle lifecycle events.",[104,42163,42165],{"id":42164},"watching-and-tracking-changes","Watching and Tracking Changes",[123,42167,36801],{"id":42168},"react-6",[27,42170,42171,42172,894],{},"React doesn’t have watchers; you use ",[22,42173,41883],{},[128,42175,42177],{"className":36884,"code":42176,"language":29196,"meta":133,"style":133},"import { useState, useEffect } from \"react\";\n\nfunction ExampleComponent() {\n    const [count, setCount] = useState(0);\n\n    useEffect(() => {\n        console.log(\"Count changed:\", count);\n    }, [count]);\n\n    return \u003Cbutton onClick={() => setCount(count + 1)}>Increment\u003C\u002Fbutton>;\n}\n",[22,42178,42179,42191,42195,42203,42227,42231,42241,42255,42260,42264,42294],{"__ignoreMap":133},[137,42180,42181,42183,42185,42187,42189],{"class":139,"line":140},[137,42182,10287],{"class":143},[137,42184,38447],{"class":157},[137,42186,10954],{"class":143},[137,42188,37226],{"class":284},[137,42190,3276],{"class":157},[137,42192,42193],{"class":139,"line":173},[137,42194,516],{"emptyLinePlaceholder":515},[137,42196,42197,42199,42201],{"class":139,"line":188},[137,42198,483],{"class":143},[137,42200,39291],{"class":147},[137,42202,275],{"class":157},[137,42204,42205,42207,42209,42211,42213,42215,42217,42219,42221,42223,42225],{"class":139,"line":269},[137,42206,4177],{"class":143},[137,42208,22130],{"class":157},[137,42210,40296],{"class":364},[137,42212,164],{"class":157},[137,42214,40301],{"class":364},[137,42216,5796],{"class":157},[137,42218,253],{"class":143},[137,42220,37266],{"class":147},[137,42222,356],{"class":157},[137,42224,6044],{"class":364},[137,42226,1502],{"class":157},[137,42228,42229],{"class":139,"line":278},[137,42230,516],{"emptyLinePlaceholder":515},[137,42232,42233,42235,42237,42239],{"class":139,"line":291},[137,42234,37281],{"class":147},[137,42236,3193],{"class":157},[137,42238,222],{"class":143},[137,42240,256],{"class":157},[137,42242,42243,42245,42247,42249,42252],{"class":139,"line":297},[137,42244,350],{"class":157},[137,42246,353],{"class":147},[137,42248,356],{"class":157},[137,42250,42251],{"class":284},"\"Count changed:\"",[137,42253,42254],{"class":157},", count);\n",[137,42256,42257],{"class":139,"line":302},[137,42258,42259],{"class":157},"    }, [count]);\n",[137,42261,42262],{"class":139,"line":662},[137,42263,516],{"emptyLinePlaceholder":515},[137,42265,42266,42268,42270,42272,42274,42276,42278,42280,42282,42284,42286,42288,42290,42292],{"class":139,"line":667},[137,42267,176],{"class":143},[137,42269,29304],{"class":157},[137,42271,8170],{"class":4036},[137,42273,40353],{"class":147},[137,42275,253],{"class":143},[137,42277,40358],{"class":157},[137,42279,222],{"class":143},[137,42281,40363],{"class":147},[137,42283,40366],{"class":157},[137,42285,182],{"class":143},[137,42287,8030],{"class":364},[137,42289,40373],{"class":157},[137,42291,8170],{"class":4036},[137,42293,29320],{"class":157},[137,42295,42296],{"class":139,"line":786},[137,42297,510],{"class":157},[123,42299,26585],{"id":42300},"vue-6",[27,42302,42303,42304,42306],{},"Vue provides a ",[22,42305,27791],{}," function to observe changes:",[128,42308,42310],{"className":4024,"code":42309,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import { ref, watch } from \"vue\";\n\n    const count = ref(0);\n\n    watch(count, (newValue) => {\n        console.log(\"Count changed:\", newValue);\n    });\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cbutton @click=\"count++\">Increment\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n",[22,42311,42312,42322,42335,42339,42355,42359,42375,42388,42392,42400,42404,42412,42430],{"__ignoreMap":133},[137,42313,42314,42316,42318,42320],{"class":139,"line":140},[137,42315,4033],{"class":157},[137,42317,4037],{"class":4036},[137,42319,9642],{"class":147},[137,42321,4053],{"class":157},[137,42323,42324,42326,42329,42331,42333],{"class":139,"line":173},[137,42325,37677],{"class":143},[137,42327,42328],{"class":157}," { ref, watch } ",[137,42330,10954],{"class":143},[137,42332,11091],{"class":284},[137,42334,3276],{"class":157},[137,42336,42337],{"class":139,"line":188},[137,42338,516],{"emptyLinePlaceholder":515},[137,42340,42341,42343,42345,42347,42349,42351,42353],{"class":139,"line":269},[137,42342,4177],{"class":143},[137,42344,40934],{"class":364},[137,42346,151],{"class":143},[137,42348,10468],{"class":147},[137,42350,356],{"class":157},[137,42352,6044],{"class":364},[137,42354,1502],{"class":157},[137,42356,42357],{"class":139,"line":278},[137,42358,516],{"emptyLinePlaceholder":515},[137,42360,42361,42364,42367,42369,42371,42373],{"class":139,"line":291},[137,42362,42363],{"class":147},"    watch",[137,42365,42366],{"class":157},"(count, (",[137,42368,6125],{"class":161},[137,42370,219],{"class":157},[137,42372,222],{"class":143},[137,42374,256],{"class":157},[137,42376,42377,42379,42381,42383,42385],{"class":139,"line":297},[137,42378,350],{"class":157},[137,42380,353],{"class":147},[137,42382,356],{"class":157},[137,42384,42251],{"class":284},[137,42386,42387],{"class":157},", newValue);\n",[137,42389,42390],{"class":139,"line":302},[137,42391,2832],{"class":157},[137,42393,42394,42396,42398],{"class":139,"line":662},[137,42395,4083],{"class":157},[137,42397,4037],{"class":4036},[137,42399,4053],{"class":157},[137,42401,42402],{"class":139,"line":667},[137,42403,516],{"emptyLinePlaceholder":515},[137,42405,42406,42408,42410],{"class":139,"line":786},[137,42407,4033],{"class":157},[137,42409,7821],{"class":4036},[137,42411,4053],{"class":157},[137,42413,42414,42416,42418,42420,42422,42424,42426,42428],{"class":139,"line":798},[137,42415,4072],{"class":157},[137,42417,8170],{"class":4036},[137,42419,40994],{"class":147},[137,42421,253],{"class":157},[137,42423,40999],{"class":284},[137,42425,41002],{"class":157},[137,42427,8170],{"class":4036},[137,42429,4053],{"class":157},[137,42431,42432,42434,42436],{"class":139,"line":803},[137,42433,4083],{"class":157},[137,42435,7821],{"class":4036},[137,42437,4053],{"class":157},[27,42439,42440],{},[42,42441,39387],{},[1003,42443,42444],{},[1006,42445,42446,42447,42449],{},"Vue’s ",[22,42448,27791],{}," directly observes reactive variables.",[104,42451,42453],{"id":42452},"conditional-rendering","Conditional Rendering",[123,42455,36801],{"id":42456},"react-7",[27,42458,42459],{},"React uses JavaScript expressions or ternary operators:",[128,42461,42463],{"className":36884,"code":42462,"language":29196,"meta":133,"style":133},"function ExampleComponent() {\n    const isLoggedIn = true;\n\n    return \u003Cp>{isLoggedIn ? \"Welcome back!\" : \"Please log in.\"}\u003C\u002Fp>;\n}\n",[22,42464,42465,42473,42486,42490,42518],{"__ignoreMap":133},[137,42466,42467,42469,42471],{"class":139,"line":140},[137,42468,483],{"class":143},[137,42470,39291],{"class":147},[137,42472,275],{"class":157},[137,42474,42475,42477,42480,42482,42484],{"class":139,"line":173},[137,42476,4177],{"class":143},[137,42478,42479],{"class":364}," isLoggedIn",[137,42481,151],{"class":143},[137,42483,14286],{"class":364},[137,42485,3276],{"class":157},[137,42487,42488],{"class":139,"line":188},[137,42489,516],{"emptyLinePlaceholder":515},[137,42491,42492,42494,42496,42498,42501,42503,42506,42508,42511,42514,42516],{"class":139,"line":269},[137,42493,176],{"class":143},[137,42495,29304],{"class":157},[137,42497,27],{"class":4036},[137,42499,42500],{"class":157},">{isLoggedIn ",[137,42502,12972],{"class":143},[137,42504,42505],{"class":284}," \"Welcome back!\"",[137,42507,26201],{"class":143},[137,42509,42510],{"class":284}," \"Please log in.\"",[137,42512,42513],{"class":157},"}\u003C\u002F",[137,42515,27],{"class":4036},[137,42517,29320],{"class":157},[137,42519,42520],{"class":139,"line":278},[137,42521,510],{"class":157},[123,42523,26585],{"id":42524},"vue-7",[27,42526,42527,42528,114,42531,894],{},"Vue uses directives like ",[22,42529,42530],{},"v-if",[22,42532,42533],{},"v-else",[27,42535,42536],{},[42,42537,39387],{},[1003,42539,42540],{},[1006,42541,42542,42543,164,42545,42547],{},"Vue uses declarative directives (",[22,42544,42530],{},[22,42546,42533],{},") for conditional rendering.",[128,42549,42551],{"className":4024,"code":42550,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    const isLoggedIn = true;\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cp v-if=\"isLoggedIn\">Welcome back!\u003C\u002Fp>\n    \u003Cp v-else>Please log in.\u003C\u002Fp>\n\u003C\u002Ftemplate>\n",[22,42552,42553,42563,42575,42583,42587,42595,42615,42630],{"__ignoreMap":133},[137,42554,42555,42557,42559,42561],{"class":139,"line":140},[137,42556,4033],{"class":157},[137,42558,4037],{"class":4036},[137,42560,9642],{"class":147},[137,42562,4053],{"class":157},[137,42564,42565,42567,42569,42571,42573],{"class":139,"line":173},[137,42566,4177],{"class":143},[137,42568,42479],{"class":364},[137,42570,151],{"class":143},[137,42572,14286],{"class":364},[137,42574,3276],{"class":157},[137,42576,42577,42579,42581],{"class":139,"line":188},[137,42578,4083],{"class":157},[137,42580,4037],{"class":4036},[137,42582,4053],{"class":157},[137,42584,42585],{"class":139,"line":269},[137,42586,516],{"emptyLinePlaceholder":515},[137,42588,42589,42591,42593],{"class":139,"line":278},[137,42590,4033],{"class":157},[137,42592,7821],{"class":4036},[137,42594,4053],{"class":157},[137,42596,42597,42599,42601,42603,42605,42608,42611,42613],{"class":139,"line":291},[137,42598,4072],{"class":157},[137,42600,27],{"class":4036},[137,42602,9989],{"class":147},[137,42604,253],{"class":157},[137,42606,42607],{"class":284},"\"isLoggedIn\"",[137,42609,42610],{"class":157},">Welcome back!\u003C\u002F",[137,42612,27],{"class":4036},[137,42614,4053],{"class":157},[137,42616,42617,42619,42621,42623,42626,42628],{"class":139,"line":297},[137,42618,4072],{"class":157},[137,42620,27],{"class":4036},[137,42622,10010],{"class":147},[137,42624,42625],{"class":157},">Please log in.\u003C\u002F",[137,42627,27],{"class":4036},[137,42629,4053],{"class":157},[137,42631,42632,42634,42636],{"class":139,"line":302},[137,42633,4083],{"class":157},[137,42635,7821],{"class":4036},[137,42637,4053],{"class":157},[104,42639,42641],{"id":42640},"passing-props-to-child-components","Passing Props to Child Components",[123,42643,42645],{"id":42644},"react-8",[42,42646,36801],{},[27,42648,42649,42650,42652],{},"In React, props are passed by adding attributes to the child component in the parent component. The child component accesses these props through the ",[22,42651,29277],{}," parameter.",[27,42654,42655],{},[42,42656,42657],{},"Parent Component",[128,42659,42661],{"className":36884,"code":42660,"language":29196,"meta":133,"style":133},"function Parent() {\n    return \u003CChild message=\"Hello from Parent!\" \u002F>;\n}\n\nexport default Parent;\n",[22,42662,42663,42672,42692,42696,42700],{"__ignoreMap":133},[137,42664,42665,42667,42670],{"class":139,"line":140},[137,42666,483],{"class":143},[137,42668,42669],{"class":147}," Parent",[137,42671,275],{"class":157},[137,42673,42674,42676,42678,42681,42684,42686,42689],{"class":139,"line":173},[137,42675,176],{"class":143},[137,42677,29304],{"class":157},[137,42679,42680],{"class":364},"Child",[137,42682,42683],{"class":147}," message",[137,42685,253],{"class":143},[137,42687,42688],{"class":284},"\"Hello from Parent!\"",[137,42690,42691],{"class":157}," \u002F>;\n",[137,42693,42694],{"class":139,"line":188},[137,42695,510],{"class":157},[137,42697,42698],{"class":139,"line":269},[137,42699,516],{"emptyLinePlaceholder":515},[137,42701,42702,42704,42706],{"class":139,"line":278},[137,42703,13456],{"class":143},[137,42705,21723],{"class":143},[137,42707,42708],{"class":157}," Parent;\n",[27,42710,42711],{},[42,42712,42713],{},"Child Component",[128,42715,42717],{"className":36884,"code":42716,"language":29196,"meta":133,"style":133},"function Child(props) {\n    return \u003Ch1>{props.message}\u003C\u002Fh1>;\n}\n\nexport default Child;\n",[22,42718,42719,42732,42747,42751,42755],{"__ignoreMap":133},[137,42720,42721,42723,42726,42728,42730],{"class":139,"line":140},[137,42722,483],{"class":143},[137,42724,42725],{"class":147}," Child",[137,42727,356],{"class":157},[137,42729,29277],{"class":161},[137,42731,170],{"class":157},[137,42733,42734,42736,42738,42740,42743,42745],{"class":139,"line":173},[137,42735,176],{"class":143},[137,42737,29304],{"class":157},[137,42739,17],{"class":4036},[137,42741,42742],{"class":157},">{props.message}\u003C\u002F",[137,42744,17],{"class":4036},[137,42746,29320],{"class":157},[137,42748,42749],{"class":139,"line":188},[137,42750,510],{"class":157},[137,42752,42753],{"class":139,"line":269},[137,42754,516],{"emptyLinePlaceholder":515},[137,42756,42757,42759,42761],{"class":139,"line":278},[137,42758,13456],{"class":143},[137,42760,21723],{"class":143},[137,42762,42763],{"class":157}," Child;\n",[123,42765,42767],{"id":42766},"vue-8",[42,42768,26585],{},[27,42770,42771],{},"In Vue, props are passed in a similar way by using attributes on the child component in the parent. However, the child component explicitly defines the props it expects.",[27,42773,42774],{},[42,42775,42657],{},[128,42777,42779],{"className":4024,"code":42778,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import Child from \".\u002FChild.vue\";\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003CChild message=\"Hello from Parent!\" \u002F>\n\u003C\u002Ftemplate>\n",[22,42780,42781,42791,42805,42813,42817,42825,42839],{"__ignoreMap":133},[137,42782,42783,42785,42787,42789],{"class":139,"line":140},[137,42784,4033],{"class":157},[137,42786,4037],{"class":4036},[137,42788,9642],{"class":147},[137,42790,4053],{"class":157},[137,42792,42793,42795,42798,42800,42803],{"class":139,"line":173},[137,42794,37677],{"class":143},[137,42796,42797],{"class":157}," Child ",[137,42799,10954],{"class":143},[137,42801,42802],{"class":284}," \".\u002FChild.vue\"",[137,42804,3276],{"class":157},[137,42806,42807,42809,42811],{"class":139,"line":188},[137,42808,4083],{"class":157},[137,42810,4037],{"class":4036},[137,42812,4053],{"class":157},[137,42814,42815],{"class":139,"line":269},[137,42816,516],{"emptyLinePlaceholder":515},[137,42818,42819,42821,42823],{"class":139,"line":278},[137,42820,4033],{"class":157},[137,42822,7821],{"class":4036},[137,42824,4053],{"class":157},[137,42826,42827,42829,42831,42833,42835,42837],{"class":139,"line":291},[137,42828,4072],{"class":157},[137,42830,42680],{"class":8180},[137,42832,42683],{"class":147},[137,42834,253],{"class":157},[137,42836,42688],{"class":284},[137,42838,4078],{"class":157},[137,42840,42841,42843,42845],{"class":139,"line":297},[137,42842,4083],{"class":157},[137,42844,7821],{"class":4036},[137,42846,4053],{"class":157},[27,42848,42849],{},[42,42850,42713],{},[128,42852,42854],{"className":4024,"code":42853,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    defineProps({\n        message: String,\n    });\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Ch1>{{ message }}\u003C\u002Fh1>\n\u003C\u002Ftemplate>\n",[22,42855,42856,42866,42872,42877,42881,42889,42893,42901,42914],{"__ignoreMap":133},[137,42857,42858,42860,42862,42864],{"class":139,"line":140},[137,42859,4033],{"class":157},[137,42861,4037],{"class":4036},[137,42863,9642],{"class":147},[137,42865,4053],{"class":157},[137,42867,42868,42870],{"class":139,"line":173},[137,42869,37695],{"class":147},[137,42871,3175],{"class":157},[137,42873,42874],{"class":139,"line":188},[137,42875,42876],{"class":157},"        message: String,\n",[137,42878,42879],{"class":139,"line":269},[137,42880,2832],{"class":157},[137,42882,42883,42885,42887],{"class":139,"line":278},[137,42884,4083],{"class":157},[137,42886,4037],{"class":4036},[137,42888,4053],{"class":157},[137,42890,42891],{"class":139,"line":291},[137,42892,516],{"emptyLinePlaceholder":515},[137,42894,42895,42897,42899],{"class":139,"line":297},[137,42896,4033],{"class":157},[137,42898,7821],{"class":4036},[137,42900,4053],{"class":157},[137,42902,42903,42905,42907,42910,42912],{"class":139,"line":302},[137,42904,4072],{"class":157},[137,42906,17],{"class":4036},[137,42908,42909],{"class":157},">{{ message }}\u003C\u002F",[137,42911,17],{"class":4036},[137,42913,4053],{"class":157},[137,42915,42916,42918,42920],{"class":139,"line":662},[137,42917,4083],{"class":157},[137,42919,7821],{"class":4036},[137,42921,4053],{"class":157},[27,42923,42924],{},[42,42925,41862],{},[1003,42927,42928,42935,42941],{},[1006,42929,42930,42931,42934],{},"Vue requires explicit prop definitions using ",[22,42932,42933],{},"defineProps",", while React doesn't require prop declarations",[1006,42936,42937,42938,42940],{},"Vue allows direct access to props as variables in the",[22,42939,7821],{},"section, while React requires accessing them through the props object unless they are destructured.",[1006,42942,42943,42944,42946],{},"Vue has built-in prop type checking with ",[22,42945,42933],{},", while React relies on PropTypes or TypeScript",[104,42948,42950],{"id":42949},"passing-components-in-slots","Passing Components in Slots",[123,42952,36801],{"id":42953},"react-9",[27,42955,42956],{},"React uses children props for dynamic content:",[128,42958,42960],{"className":36884,"code":42959,"language":29196,"meta":133,"style":133},"function Modal({ children }) {\n    return \u003Cdiv className=\"modal\">{children}\u003C\u002Fdiv>;\n}\n\nfunction ExampleComponent() {\n    return (\n        \u003CModal>\n            \u003Ch1>Modal Content\u003C\u002Fh1>\n        \u003C\u002FModal>\n    );\n}\n",[22,42961,42962,42975,42997,43001,43005,43013,43019,43028,43041,43049,43053],{"__ignoreMap":133},[137,42963,42964,42966,42969,42971,42973],{"class":139,"line":140},[137,42965,483],{"class":143},[137,42967,42968],{"class":147}," Modal",[137,42970,36896],{"class":157},[137,42972,37244],{"class":161},[137,42974,36901],{"class":157},[137,42976,42977,42979,42981,42983,42985,42987,42990,42993,42995],{"class":139,"line":173},[137,42978,176],{"class":143},[137,42980,29304],{"class":157},[137,42982,8330],{"class":4036},[137,42984,36916],{"class":147},[137,42986,253],{"class":143},[137,42988,42989],{"class":284},"\"modal\"",[137,42991,42992],{"class":157},">{children}\u003C\u002F",[137,42994,8330],{"class":4036},[137,42996,29320],{"class":157},[137,42998,42999],{"class":139,"line":188},[137,43000,510],{"class":157},[137,43002,43003],{"class":139,"line":269},[137,43004,516],{"emptyLinePlaceholder":515},[137,43006,43007,43009,43011],{"class":139,"line":278},[137,43008,483],{"class":143},[137,43010,39291],{"class":147},[137,43012,275],{"class":157},[137,43014,43015,43017],{"class":139,"line":291},[137,43016,176],{"class":143},[137,43018,30009],{"class":157},[137,43020,43021,43023,43026],{"class":139,"line":297},[137,43022,9826],{"class":157},[137,43024,43025],{"class":364},"Modal",[137,43027,4053],{"class":157},[137,43029,43030,43032,43034,43037,43039],{"class":139,"line":302},[137,43031,23852],{"class":157},[137,43033,17],{"class":4036},[137,43035,43036],{"class":157},">Modal Content\u003C\u002F",[137,43038,17],{"class":4036},[137,43040,4053],{"class":157},[137,43042,43043,43045,43047],{"class":139,"line":662},[137,43044,9843],{"class":157},[137,43046,43025],{"class":364},[137,43048,4053],{"class":157},[137,43050,43051],{"class":139,"line":667},[137,43052,11875],{"class":157},[137,43054,43055],{"class":139,"line":786},[137,43056,510],{"class":157},[123,43058,26585],{"id":43059},"vue-9",[27,43061,43062,43063,43066],{},"In Vue, components need to be declared in separate files. We use the ",[22,43064,43065],{},"\u003Cslot>"," component as a placeholder where we expect the child component to be passed.",[27,43068,43069],{},[42,43070,43071,43072,14105],{},"Modal Component (",[22,43073,43074],{},"Modal.vue",[128,43076,43078],{"className":4024,"code":43077,"language":4026,"meta":133,"style":133},"\u003Ctemplate>\n    \u003Cdiv class=\"modal\">\n        \u003Cslot>\u003C\u002Fslot>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,43079,43080,43088,43102,43114,43122],{"__ignoreMap":133},[137,43081,43082,43084,43086],{"class":139,"line":140},[137,43083,4033],{"class":157},[137,43085,7821],{"class":4036},[137,43087,4053],{"class":157},[137,43089,43090,43092,43094,43096,43098,43100],{"class":139,"line":173},[137,43091,4072],{"class":157},[137,43093,8330],{"class":4036},[137,43095,7832],{"class":147},[137,43097,253],{"class":157},[137,43099,42989],{"class":284},[137,43101,4053],{"class":157},[137,43103,43104,43106,43108,43110,43112],{"class":139,"line":188},[137,43105,9826],{"class":157},[137,43107,38262],{"class":4036},[137,43109,4048],{"class":157},[137,43111,38262],{"class":4036},[137,43113,4053],{"class":157},[137,43115,43116,43118,43120],{"class":139,"line":269},[137,43117,8374],{"class":157},[137,43119,8330],{"class":4036},[137,43121,4053],{"class":157},[137,43123,43124,43126,43128],{"class":139,"line":278},[137,43125,4083],{"class":157},[137,43127,7821],{"class":4036},[137,43129,4053],{"class":157},[27,43131,43132],{},[42,43133,42657],{},[128,43135,43137],{"className":4024,"code":43136,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import Modal from \".\u002FModal.vue\";\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003CModal>\n        \u003Ch1>Modal Content\u003C\u002Fh1>\n    \u003C\u002FModal>\n\u003C\u002Ftemplate>\n",[22,43138,43139,43149,43163,43171,43175,43183,43191,43203,43211],{"__ignoreMap":133},[137,43140,43141,43143,43145,43147],{"class":139,"line":140},[137,43142,4033],{"class":157},[137,43144,4037],{"class":4036},[137,43146,9642],{"class":147},[137,43148,4053],{"class":157},[137,43150,43151,43153,43156,43158,43161],{"class":139,"line":173},[137,43152,37677],{"class":143},[137,43154,43155],{"class":157}," Modal ",[137,43157,10954],{"class":143},[137,43159,43160],{"class":284}," \".\u002FModal.vue\"",[137,43162,3276],{"class":157},[137,43164,43165,43167,43169],{"class":139,"line":188},[137,43166,4083],{"class":157},[137,43168,4037],{"class":4036},[137,43170,4053],{"class":157},[137,43172,43173],{"class":139,"line":269},[137,43174,516],{"emptyLinePlaceholder":515},[137,43176,43177,43179,43181],{"class":139,"line":278},[137,43178,4033],{"class":157},[137,43180,7821],{"class":4036},[137,43182,4053],{"class":157},[137,43184,43185,43187,43189],{"class":139,"line":291},[137,43186,4072],{"class":157},[137,43188,43025],{"class":8180},[137,43190,4053],{"class":157},[137,43192,43193,43195,43197,43199,43201],{"class":139,"line":297},[137,43194,9826],{"class":157},[137,43196,17],{"class":4036},[137,43198,43036],{"class":157},[137,43200,17],{"class":4036},[137,43202,4053],{"class":157},[137,43204,43205,43207,43209],{"class":139,"line":302},[137,43206,8374],{"class":157},[137,43208,43025],{"class":8180},[137,43210,4053],{"class":157},[137,43212,43213,43215,43217],{"class":139,"line":662},[137,43214,4083],{"class":157},[137,43216,7821],{"class":4036},[137,43218,4053],{"class":157},[27,43220,43221],{},[42,43222,43223],{},"Vue Named Slots:",[27,43225,43226],{},"With Vue named slots, we can define multiple slot places in the parent component and pass different children.",[27,43228,43229],{},[42,43230,43231],{},[22,43232,43074],{},[128,43234,43236],{"className":4024,"code":43235,"language":4026,"meta":133,"style":133},"\u003Ctemplate>\n    \u003Cdiv class=\"modal\">\n        \u003Cslot name=\"header\">\u003C\u002Fslot>\n        \u003Cslot>\u003C\u002Fslot>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,43237,43238,43246,43260,43278,43290,43298],{"__ignoreMap":133},[137,43239,43240,43242,43244],{"class":139,"line":140},[137,43241,4033],{"class":157},[137,43243,7821],{"class":4036},[137,43245,4053],{"class":157},[137,43247,43248,43250,43252,43254,43256,43258],{"class":139,"line":173},[137,43249,4072],{"class":157},[137,43251,8330],{"class":4036},[137,43253,7832],{"class":147},[137,43255,253],{"class":157},[137,43257,42989],{"class":284},[137,43259,4053],{"class":157},[137,43261,43262,43264,43266,43268,43270,43272,43274,43276],{"class":139,"line":188},[137,43263,9826],{"class":157},[137,43265,38262],{"class":4036},[137,43267,891],{"class":147},[137,43269,253],{"class":157},[137,43271,39659],{"class":284},[137,43273,4048],{"class":157},[137,43275,38262],{"class":4036},[137,43277,4053],{"class":157},[137,43279,43280,43282,43284,43286,43288],{"class":139,"line":269},[137,43281,9826],{"class":157},[137,43283,38262],{"class":4036},[137,43285,4048],{"class":157},[137,43287,38262],{"class":4036},[137,43289,4053],{"class":157},[137,43291,43292,43294,43296],{"class":139,"line":278},[137,43293,8374],{"class":157},[137,43295,8330],{"class":4036},[137,43297,4053],{"class":157},[137,43299,43300,43302,43304],{"class":139,"line":291},[137,43301,4083],{"class":157},[137,43303,7821],{"class":4036},[137,43305,4053],{"class":157},[27,43307,43308],{},[42,43309,42657],{},[128,43311,43313],{"className":4024,"code":43312,"language":4026,"meta":133,"style":133},"\u003Ctemplate>\n    \u003CModal>\n        \u003Ctemplate #header>\n            \u003Ch1>Modal Header\u003C\u002Fh1>\n        \u003C\u002Ftemplate>\n        \u003Cp>Modal Body\u003C\u002Fp>\n    \u003C\u002FModal>\n\u003C\u002Ftemplate>\n",[22,43314,43315,43323,43331,43342,43355,43363,43376,43384],{"__ignoreMap":133},[137,43316,43317,43319,43321],{"class":139,"line":140},[137,43318,4033],{"class":157},[137,43320,7821],{"class":4036},[137,43322,4053],{"class":157},[137,43324,43325,43327,43329],{"class":139,"line":173},[137,43326,4072],{"class":157},[137,43328,43025],{"class":8180},[137,43330,4053],{"class":157},[137,43332,43333,43335,43337,43340],{"class":139,"line":188},[137,43334,9826],{"class":157},[137,43336,7821],{"class":4036},[137,43338,43339],{"class":147}," #header",[137,43341,4053],{"class":157},[137,43343,43344,43346,43348,43351,43353],{"class":139,"line":269},[137,43345,23852],{"class":157},[137,43347,17],{"class":4036},[137,43349,43350],{"class":157},">Modal Header\u003C\u002F",[137,43352,17],{"class":4036},[137,43354,4053],{"class":157},[137,43356,43357,43359,43361],{"class":139,"line":278},[137,43358,9843],{"class":157},[137,43360,7821],{"class":4036},[137,43362,4053],{"class":157},[137,43364,43365,43367,43369,43372,43374],{"class":139,"line":291},[137,43366,9826],{"class":157},[137,43368,27],{"class":4036},[137,43370,43371],{"class":157},">Modal Body\u003C\u002F",[137,43373,27],{"class":4036},[137,43375,4053],{"class":157},[137,43377,43378,43380,43382],{"class":139,"line":297},[137,43379,8374],{"class":157},[137,43381,43025],{"class":8180},[137,43383,4053],{"class":157},[137,43385,43386,43388,43390],{"class":139,"line":302},[137,43387,4083],{"class":157},[137,43389,7821],{"class":4036},[137,43391,4053],{"class":157},[27,43393,43394],{},[42,43395,39387],{},[1003,43397,43398],{},[1006,43399,42446,43400,43402],{},[22,43401,38262],{}," allows for flexible content placement within components.",[104,43404,43406],{"id":43405},"handling-events-between-parent-and-child-components","Handling Events Between Parent and Child Components",[123,43408,36801],{"id":43409},"react-10",[27,43411,43412],{},"React passes functions as props to children:",[128,43414,43416],{"className":36884,"code":43415,"language":29196,"meta":133,"style":133},"function Child({ onClick }) {\n    return \u003Cbutton onClick={onClick}>Click me\u003C\u002Fbutton>;\n}\n\nfunction ExampleComponent() {\n    const handleClick = () => alert(\"Button clicked!\");\n    return \u003CChild onClick={handleClick} \u002F>;\n}\n",[22,43417,43418,43430,43449,43453,43457,43465,43488,43503],{"__ignoreMap":133},[137,43419,43420,43422,43424,43426,43428],{"class":139,"line":140},[137,43421,483],{"class":143},[137,43423,42725],{"class":147},[137,43425,36896],{"class":157},[137,43427,30096],{"class":161},[137,43429,36901],{"class":157},[137,43431,43432,43434,43436,43438,43440,43442,43445,43447],{"class":139,"line":173},[137,43433,176],{"class":143},[137,43435,29304],{"class":157},[137,43437,8170],{"class":4036},[137,43439,40353],{"class":147},[137,43441,253],{"class":143},[137,43443,43444],{"class":157},"{onClick}>Click me\u003C\u002F",[137,43446,8170],{"class":4036},[137,43448,29320],{"class":157},[137,43450,43451],{"class":139,"line":188},[137,43452,510],{"class":157},[137,43454,43455],{"class":139,"line":269},[137,43456,516],{"emptyLinePlaceholder":515},[137,43458,43459,43461,43463],{"class":139,"line":278},[137,43460,483],{"class":143},[137,43462,39291],{"class":147},[137,43464,275],{"class":157},[137,43466,43467,43469,43472,43474,43476,43478,43481,43483,43486],{"class":139,"line":291},[137,43468,4177],{"class":143},[137,43470,43471],{"class":147}," handleClick",[137,43473,151],{"class":143},[137,43475,1484],{"class":157},[137,43477,222],{"class":143},[137,43479,43480],{"class":147}," alert",[137,43482,356],{"class":157},[137,43484,43485],{"class":284},"\"Button clicked!\"",[137,43487,1502],{"class":157},[137,43489,43490,43492,43494,43496,43498,43500],{"class":139,"line":297},[137,43491,176],{"class":143},[137,43493,29304],{"class":157},[137,43495,42680],{"class":364},[137,43497,40353],{"class":147},[137,43499,253],{"class":143},[137,43501,43502],{"class":157},"{handleClick} \u002F>;\n",[137,43504,43505],{"class":139,"line":302},[137,43506,510],{"class":157},[123,43508,26585],{"id":43509},"vue-10",[27,43511,39860,43512,43515],{},[22,43513,43514],{},"emit"," for event communication:",[27,43517,43518],{},[42,43519,43520],{},"Children Component",[128,43522,43524],{"className":4024,"code":43523,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    const emit = defineEmits([\"clicked\"]);\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cbutton @click=\"emit('clicked')\">Click me\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n",[22,43525,43526,43536,43557,43565,43569,43577,43597],{"__ignoreMap":133},[137,43527,43528,43530,43532,43534],{"class":139,"line":140},[137,43529,4033],{"class":157},[137,43531,4037],{"class":4036},[137,43533,9642],{"class":147},[137,43535,4053],{"class":157},[137,43537,43538,43540,43543,43545,43548,43551,43554],{"class":139,"line":173},[137,43539,4177],{"class":143},[137,43541,43542],{"class":364}," emit",[137,43544,151],{"class":143},[137,43546,43547],{"class":147}," defineEmits",[137,43549,43550],{"class":157},"([",[137,43552,43553],{"class":284},"\"clicked\"",[137,43555,43556],{"class":157},"]);\n",[137,43558,43559,43561,43563],{"class":139,"line":188},[137,43560,4083],{"class":157},[137,43562,4037],{"class":4036},[137,43564,4053],{"class":157},[137,43566,43567],{"class":139,"line":269},[137,43568,516],{"emptyLinePlaceholder":515},[137,43570,43571,43573,43575],{"class":139,"line":278},[137,43572,4033],{"class":157},[137,43574,7821],{"class":4036},[137,43576,4053],{"class":157},[137,43578,43579,43581,43583,43585,43587,43590,43593,43595],{"class":139,"line":291},[137,43580,4072],{"class":157},[137,43582,8170],{"class":4036},[137,43584,40994],{"class":147},[137,43586,253],{"class":157},[137,43588,43589],{"class":284},"\"emit('clicked')\"",[137,43591,43592],{"class":157},">Click me\u003C\u002F",[137,43594,8170],{"class":4036},[137,43596,4053],{"class":157},[137,43598,43599,43601,43603],{"class":139,"line":297},[137,43600,4083],{"class":157},[137,43602,7821],{"class":4036},[137,43604,4053],{"class":157},[27,43606,43607],{},[42,43608,42657],{},[128,43610,43612],{"className":4024,"code":43611,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import Children from \".\u002FChildren.vue\";\n\n    const handleClick = () => console.log(\"Button clicked!\");\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003CChildren @clicked=\"handleClick\" \u002F>\n\u003C\u002Ftemplate>\n",[22,43613,43614,43624,43638,43642,43665,43673,43677,43685,43702],{"__ignoreMap":133},[137,43615,43616,43618,43620,43622],{"class":139,"line":140},[137,43617,4033],{"class":157},[137,43619,4037],{"class":4036},[137,43621,9642],{"class":147},[137,43623,4053],{"class":157},[137,43625,43626,43628,43631,43633,43636],{"class":139,"line":173},[137,43627,37677],{"class":143},[137,43629,43630],{"class":157}," Children ",[137,43632,10954],{"class":143},[137,43634,43635],{"class":284}," \".\u002FChildren.vue\"",[137,43637,3276],{"class":157},[137,43639,43640],{"class":139,"line":188},[137,43641,516],{"emptyLinePlaceholder":515},[137,43643,43644,43646,43648,43650,43652,43654,43657,43659,43661,43663],{"class":139,"line":269},[137,43645,4177],{"class":143},[137,43647,43471],{"class":147},[137,43649,151],{"class":143},[137,43651,1484],{"class":157},[137,43653,222],{"class":143},[137,43655,43656],{"class":157}," console.",[137,43658,353],{"class":147},[137,43660,356],{"class":157},[137,43662,43485],{"class":284},[137,43664,1502],{"class":157},[137,43666,43667,43669,43671],{"class":139,"line":278},[137,43668,4083],{"class":157},[137,43670,4037],{"class":4036},[137,43672,4053],{"class":157},[137,43674,43675],{"class":139,"line":291},[137,43676,516],{"emptyLinePlaceholder":515},[137,43678,43679,43681,43683],{"class":139,"line":297},[137,43680,4033],{"class":157},[137,43682,7821],{"class":4036},[137,43684,4053],{"class":157},[137,43686,43687,43689,43692,43695,43697,43700],{"class":139,"line":302},[137,43688,4072],{"class":157},[137,43690,43691],{"class":8180},"Children",[137,43693,43694],{"class":147}," @clicked",[137,43696,253],{"class":157},[137,43698,43699],{"class":284},"\"handleClick\"",[137,43701,4078],{"class":157},[137,43703,43704,43706,43708],{"class":139,"line":662},[137,43705,4083],{"class":157},[137,43707,7821],{"class":4036},[137,43709,4053],{"class":157},[27,43711,43712],{},[42,43713,39387],{},[1003,43715,43716],{},[1006,43717,42446,43718,43720],{},[22,43719,43514],{}," is designed for child-to-parent communication.",[104,43722,43724],{"id":43723},"accessing-dom-elements-with-refs","Accessing DOM Elements with Refs",[123,43726,36801],{"id":43727},"react-11",[27,43729,40253,43730,43733],{},[22,43731,43732],{},"useRef"," to access DOM elements:",[128,43735,43737],{"className":36884,"code":43736,"language":29196,"meta":133,"style":133},"import { useRef } from \"react\";\n\nfunction ExampleComponent() {\n    const inputRef = useRef();\n\n    const focusInput = () => {\n        inputRef.current.focus();\n    };\n\n    return (\n        \u003Cdiv>\n            \u003Cinput ref={inputRef} type=\"text\" \u002F>\n            \u003Cbutton onClick={focusInput}>Focus Input\u003C\u002Fbutton>\n        \u003C\u002Fdiv>\n    );\n}\n",[22,43738,43739,43752,43756,43764,43778,43782,43797,43807,43811,43815,43821,43829,43850,43867,43875,43879],{"__ignoreMap":133},[137,43740,43741,43743,43746,43748,43750],{"class":139,"line":140},[137,43742,10287],{"class":143},[137,43744,43745],{"class":157}," { useRef } ",[137,43747,10954],{"class":143},[137,43749,37226],{"class":284},[137,43751,3276],{"class":157},[137,43753,43754],{"class":139,"line":173},[137,43755,516],{"emptyLinePlaceholder":515},[137,43757,43758,43760,43762],{"class":139,"line":188},[137,43759,483],{"class":143},[137,43761,39291],{"class":147},[137,43763,275],{"class":157},[137,43765,43766,43768,43771,43773,43776],{"class":139,"line":269},[137,43767,4177],{"class":143},[137,43769,43770],{"class":364}," inputRef",[137,43772,151],{"class":143},[137,43774,43775],{"class":147}," useRef",[137,43777,924],{"class":157},[137,43779,43780],{"class":139,"line":278},[137,43781,516],{"emptyLinePlaceholder":515},[137,43783,43784,43786,43789,43791,43793,43795],{"class":139,"line":291},[137,43785,4177],{"class":143},[137,43787,43788],{"class":147}," focusInput",[137,43790,151],{"class":143},[137,43792,1484],{"class":157},[137,43794,222],{"class":143},[137,43796,256],{"class":157},[137,43798,43799,43802,43805],{"class":139,"line":297},[137,43800,43801],{"class":157},"        inputRef.current.",[137,43803,43804],{"class":147},"focus",[137,43806,924],{"class":157},[137,43808,43809],{"class":139,"line":302},[137,43810,1892],{"class":157},[137,43812,43813],{"class":139,"line":662},[137,43814,516],{"emptyLinePlaceholder":515},[137,43816,43817,43819],{"class":139,"line":667},[137,43818,176],{"class":143},[137,43820,30009],{"class":157},[137,43822,43823,43825,43827],{"class":139,"line":786},[137,43824,9826],{"class":157},[137,43826,8330],{"class":4036},[137,43828,4053],{"class":157},[137,43830,43831,43833,43835,43837,43839,43842,43844,43846,43848],{"class":139,"line":798},[137,43832,23852],{"class":157},[137,43834,8520],{"class":4036},[137,43836,10468],{"class":147},[137,43838,253],{"class":143},[137,43840,43841],{"class":157},"{inputRef} ",[137,43843,20355],{"class":147},[137,43845,253],{"class":143},[137,43847,7837],{"class":284},[137,43849,4078],{"class":157},[137,43851,43852,43854,43856,43858,43860,43863,43865],{"class":139,"line":803},[137,43853,23852],{"class":157},[137,43855,8170],{"class":4036},[137,43857,40353],{"class":147},[137,43859,253],{"class":143},[137,43861,43862],{"class":157},"{focusInput}>Focus Input\u003C\u002F",[137,43864,8170],{"class":4036},[137,43866,4053],{"class":157},[137,43868,43869,43871,43873],{"class":139,"line":931},[137,43870,9843],{"class":157},[137,43872,8330],{"class":4036},[137,43874,4053],{"class":157},[137,43876,43877],{"class":139,"line":1568},[137,43878,11875],{"class":157},[137,43880,43881],{"class":139,"line":1573},[137,43882,510],{"class":157},[123,43884,26585],{"id":43885},"vue-11",[27,43887,43888],{},"Vue uses template refs to access DOM elements:",[128,43890,43892],{"className":4024,"code":43891,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import { ref } from \"vue\";\n\n    const inputRef = ref();\n\n    const focusInput = () => {\n        inputRef.value.focus();\n    };\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv>\n        \u003Cinput ref=\"inputRef\" type=\"text\" \u002F>\n        \u003Cbutton @click=\"focusInput\">Focus Input\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,43893,43894,43904,43916,43920,43932,43936,43950,43959,43963,43971,43975,43983,43991,44012,44032,44040],{"__ignoreMap":133},[137,43895,43896,43898,43900,43902],{"class":139,"line":140},[137,43897,4033],{"class":157},[137,43899,4037],{"class":4036},[137,43901,9642],{"class":147},[137,43903,4053],{"class":157},[137,43905,43906,43908,43910,43912,43914],{"class":139,"line":173},[137,43907,37677],{"class":143},[137,43909,40917],{"class":157},[137,43911,10954],{"class":143},[137,43913,11091],{"class":284},[137,43915,3276],{"class":157},[137,43917,43918],{"class":139,"line":188},[137,43919,516],{"emptyLinePlaceholder":515},[137,43921,43922,43924,43926,43928,43930],{"class":139,"line":269},[137,43923,4177],{"class":143},[137,43925,43770],{"class":364},[137,43927,151],{"class":143},[137,43929,10468],{"class":147},[137,43931,924],{"class":157},[137,43933,43934],{"class":139,"line":278},[137,43935,516],{"emptyLinePlaceholder":515},[137,43937,43938,43940,43942,43944,43946,43948],{"class":139,"line":291},[137,43939,4177],{"class":143},[137,43941,43788],{"class":147},[137,43943,151],{"class":143},[137,43945,1484],{"class":157},[137,43947,222],{"class":143},[137,43949,256],{"class":157},[137,43951,43952,43955,43957],{"class":139,"line":297},[137,43953,43954],{"class":157},"        inputRef.value.",[137,43956,43804],{"class":147},[137,43958,924],{"class":157},[137,43960,43961],{"class":139,"line":302},[137,43962,1892],{"class":157},[137,43964,43965,43967,43969],{"class":139,"line":662},[137,43966,4083],{"class":157},[137,43968,4037],{"class":4036},[137,43970,4053],{"class":157},[137,43972,43973],{"class":139,"line":667},[137,43974,516],{"emptyLinePlaceholder":515},[137,43976,43977,43979,43981],{"class":139,"line":786},[137,43978,4033],{"class":157},[137,43980,7821],{"class":4036},[137,43982,4053],{"class":157},[137,43984,43985,43987,43989],{"class":139,"line":798},[137,43986,4072],{"class":157},[137,43988,8330],{"class":4036},[137,43990,4053],{"class":157},[137,43992,43993,43995,43997,43999,44001,44004,44006,44008,44010],{"class":139,"line":803},[137,43994,9826],{"class":157},[137,43996,8520],{"class":4036},[137,43998,10468],{"class":147},[137,44000,253],{"class":157},[137,44002,44003],{"class":284},"\"inputRef\"",[137,44005,25639],{"class":147},[137,44007,253],{"class":157},[137,44009,7837],{"class":284},[137,44011,4078],{"class":157},[137,44013,44014,44016,44018,44020,44022,44025,44028,44030],{"class":139,"line":931},[137,44015,9826],{"class":157},[137,44017,8170],{"class":4036},[137,44019,40994],{"class":147},[137,44021,253],{"class":157},[137,44023,44024],{"class":284},"\"focusInput\"",[137,44026,44027],{"class":157},">Focus Input\u003C\u002F",[137,44029,8170],{"class":4036},[137,44031,4053],{"class":157},[137,44033,44034,44036,44038],{"class":139,"line":1568},[137,44035,8374],{"class":157},[137,44037,8330],{"class":4036},[137,44039,4053],{"class":157},[137,44041,44042,44044,44046],{"class":139,"line":1573},[137,44043,4083],{"class":157},[137,44045,7821],{"class":4036},[137,44047,4053],{"class":157},[27,44049,44050],{},[42,44051,39387],{},[1003,44053,44054,44062],{},[1006,44055,42446,44056,44058,44059,44061],{},[22,44057,27815],{}," is bound to DOM elements using the ",[22,44060,27815],{}," attribute in templates.",[1006,44063,44064,44065,1017],{},"Access the element via ",[22,44066,44067],{},"ref.value",[104,44069,44071],{"id":44070},"reusable-logic","Reusable Logic",[123,44073,36801],{"id":44074},"react-12",[27,44076,44077],{},"React uses custom hooks for reusable logic:",[128,44079,44081],{"className":36884,"code":44080,"language":29196,"meta":133,"style":133},"function useCounter() {\n    const [count, setCount] = useState(0);\n    const increment = () => setCount(count + 1);\n    return { count, increment };\n}\n\nfunction ExampleComponent() {\n    const { count, increment } = useCounter();\n\n    return \u003Cbutton onClick={increment}>Count: {count}\u003C\u002Fbutton>;\n}\n",[22,44082,44083,44092,44116,44138,44145,44149,44153,44161,44182,44186,44205],{"__ignoreMap":133},[137,44084,44085,44087,44090],{"class":139,"line":140},[137,44086,483],{"class":143},[137,44088,44089],{"class":147}," useCounter",[137,44091,275],{"class":157},[137,44093,44094,44096,44098,44100,44102,44104,44106,44108,44110,44112,44114],{"class":139,"line":173},[137,44095,4177],{"class":143},[137,44097,22130],{"class":157},[137,44099,40296],{"class":364},[137,44101,164],{"class":157},[137,44103,40301],{"class":364},[137,44105,5796],{"class":157},[137,44107,253],{"class":143},[137,44109,37266],{"class":147},[137,44111,356],{"class":157},[137,44113,6044],{"class":364},[137,44115,1502],{"class":157},[137,44117,44118,44120,44122,44124,44126,44128,44130,44132,44134,44136],{"class":139,"line":188},[137,44119,4177],{"class":143},[137,44121,41108],{"class":147},[137,44123,151],{"class":143},[137,44125,1484],{"class":157},[137,44127,222],{"class":143},[137,44129,40363],{"class":147},[137,44131,40366],{"class":157},[137,44133,182],{"class":143},[137,44135,8030],{"class":364},[137,44137,1502],{"class":157},[137,44139,44140,44142],{"class":139,"line":269},[137,44141,176],{"class":143},[137,44143,44144],{"class":157}," { count, increment };\n",[137,44146,44147],{"class":139,"line":278},[137,44148,510],{"class":157},[137,44150,44151],{"class":139,"line":291},[137,44152,516],{"emptyLinePlaceholder":515},[137,44154,44155,44157,44159],{"class":139,"line":297},[137,44156,483],{"class":143},[137,44158,39291],{"class":147},[137,44160,275],{"class":157},[137,44162,44163,44165,44167,44169,44171,44174,44176,44178,44180],{"class":139,"line":302},[137,44164,4177],{"class":143},[137,44166,8906],{"class":157},[137,44168,40296],{"class":364},[137,44170,164],{"class":157},[137,44172,44173],{"class":364},"increment",[137,44175,8911],{"class":157},[137,44177,253],{"class":143},[137,44179,44089],{"class":147},[137,44181,924],{"class":157},[137,44183,44184],{"class":139,"line":662},[137,44185,516],{"emptyLinePlaceholder":515},[137,44187,44188,44190,44192,44194,44196,44198,44201,44203],{"class":139,"line":667},[137,44189,176],{"class":143},[137,44191,29304],{"class":157},[137,44193,8170],{"class":4036},[137,44195,40353],{"class":147},[137,44197,253],{"class":143},[137,44199,44200],{"class":157},"{increment}>Count: {count}\u003C\u002F",[137,44202,8170],{"class":4036},[137,44204,29320],{"class":157},[137,44206,44207],{"class":139,"line":786},[137,44208,510],{"class":157},[123,44210,26585],{"id":44211},"vue-12",[27,44213,44214],{},"Vue uses composables for reusable logic:",[27,44216,44217],{},[22,44218,44219],{},"composables\u002FuseCounter.js",[128,44221,44223],{"className":36884,"code":44222,"language":29196,"meta":133,"style":133},"import { ref } from \"vue\";\n\nexport function useCounter() {\n    const count = ref(0);\n    const increment = () => count.value++;\n    return { count, increment };\n}\n",[22,44224,44225,44237,44241,44251,44267,44286,44292],{"__ignoreMap":133},[137,44226,44227,44229,44231,44233,44235],{"class":139,"line":140},[137,44228,10287],{"class":143},[137,44230,40917],{"class":157},[137,44232,10954],{"class":143},[137,44234,11091],{"class":284},[137,44236,3276],{"class":157},[137,44238,44239],{"class":139,"line":173},[137,44240,516],{"emptyLinePlaceholder":515},[137,44242,44243,44245,44247,44249],{"class":139,"line":188},[137,44244,13456],{"class":143},[137,44246,154],{"class":143},[137,44248,44089],{"class":147},[137,44250,275],{"class":157},[137,44252,44253,44255,44257,44259,44261,44263,44265],{"class":139,"line":269},[137,44254,4177],{"class":143},[137,44256,40934],{"class":364},[137,44258,151],{"class":143},[137,44260,10468],{"class":147},[137,44262,356],{"class":157},[137,44264,6044],{"class":364},[137,44266,1502],{"class":157},[137,44268,44269,44271,44273,44275,44277,44279,44282,44284],{"class":139,"line":278},[137,44270,4177],{"class":143},[137,44272,41108],{"class":147},[137,44274,151],{"class":143},[137,44276,1484],{"class":157},[137,44278,222],{"class":143},[137,44280,44281],{"class":157}," count.value",[137,44283,12683],{"class":143},[137,44285,3276],{"class":157},[137,44287,44288,44290],{"class":139,"line":291},[137,44289,176],{"class":143},[137,44291,44144],{"class":157},[137,44293,44294],{"class":139,"line":297},[137,44295,510],{"class":157},[27,44297,44298],{},[22,44299,44300],{},"Component.vue",[128,44302,44304],{"className":4024,"code":44303,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import { useCounter } from \".\u002Fcomposables\u002FuseCounter\";\n\n    const { count, increment } = useCounter();\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cbutton @click=\"increment\">Count: {{ count }}\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n",[22,44305,44306,44316,44330,44334,44354,44362,44366,44374,44392],{"__ignoreMap":133},[137,44307,44308,44310,44312,44314],{"class":139,"line":140},[137,44309,4033],{"class":157},[137,44311,4037],{"class":4036},[137,44313,9642],{"class":147},[137,44315,4053],{"class":157},[137,44317,44318,44320,44323,44325,44328],{"class":139,"line":173},[137,44319,37677],{"class":143},[137,44321,44322],{"class":157}," { useCounter } ",[137,44324,10954],{"class":143},[137,44326,44327],{"class":284}," \".\u002Fcomposables\u002FuseCounter\"",[137,44329,3276],{"class":157},[137,44331,44332],{"class":139,"line":188},[137,44333,516],{"emptyLinePlaceholder":515},[137,44335,44336,44338,44340,44342,44344,44346,44348,44350,44352],{"class":139,"line":269},[137,44337,4177],{"class":143},[137,44339,8906],{"class":157},[137,44341,40296],{"class":364},[137,44343,164],{"class":157},[137,44345,44173],{"class":364},[137,44347,8911],{"class":157},[137,44349,253],{"class":143},[137,44351,44089],{"class":147},[137,44353,924],{"class":157},[137,44355,44356,44358,44360],{"class":139,"line":278},[137,44357,4083],{"class":157},[137,44359,4037],{"class":4036},[137,44361,4053],{"class":157},[137,44363,44364],{"class":139,"line":291},[137,44365,516],{"emptyLinePlaceholder":515},[137,44367,44368,44370,44372],{"class":139,"line":297},[137,44369,4033],{"class":157},[137,44371,7821],{"class":4036},[137,44373,4053],{"class":157},[137,44375,44376,44378,44380,44382,44384,44386,44388,44390],{"class":139,"line":302},[137,44377,4072],{"class":157},[137,44379,8170],{"class":4036},[137,44381,40994],{"class":147},[137,44383,253],{"class":157},[137,44385,40624],{"class":284},[137,44387,40981],{"class":157},[137,44389,8170],{"class":4036},[137,44391,4053],{"class":157},[137,44393,44394,44396,44398],{"class":139,"line":662},[137,44395,4083],{"class":157},[137,44397,7821],{"class":4036},[137,44399,4053],{"class":157},[27,44401,44402],{},[42,44403,39387],{},[1003,44405,44406,44409,44412],{},[1006,44407,44408],{},"Composables are plain functions that return reactive state and methods.",[1006,44410,44411],{},"They are imported and used like hooks in React.",[1006,44413,44414,44415],{},"Even though React hooks and Vue composables are very similar, they work fundamentally differently under the hood. For example:\n",[1003,44416,44417,44420],{},[1006,44418,44419],{},"In Vue, the reactive state defined in a composable is shared and persistent when imported across multiple components. This allows composables to act as a lightweight state management solution, eliminating the need for external libraries like Redux for many use cases.",[1006,44421,44422],{},"Vue composables do not rely on a component's lifecycle or context, unlike React hooks which must be used within a component or another hook. This makes composables more flexible and usable in non-component logic like services or standalone utilities.",[123,44424,44426],{"id":44425},"shared-state-between-components-with-vue-composables","Shared State Between Components with Vue Composables",[27,44428,44429,44430,44432],{},"Here’s an example of a Vue composable that creates a shared counter state. This composable will be imported and used in two different components, and the state (",[22,44431,40296],{},") will be persistent across both components:",[27,44434,44435],{},[22,44436,44437],{},"composables\u002FuseSharedCounter.js",[128,44439,44441],{"className":36884,"code":44440,"language":29196,"meta":133,"style":133},"import { ref } from \"vue\";\n\nconst count = ref(0);\n\nexport function useSharedCounter() {\n    const increment = () => count.value++;\n    const decrement = () => count.value--;\n    return { count, increment, decrement };\n}\n",[22,44442,44443,44455,44459,44475,44479,44490,44508,44528,44535],{"__ignoreMap":133},[137,44444,44445,44447,44449,44451,44453],{"class":139,"line":140},[137,44446,10287],{"class":143},[137,44448,40917],{"class":157},[137,44450,10954],{"class":143},[137,44452,11091],{"class":284},[137,44454,3276],{"class":157},[137,44456,44457],{"class":139,"line":173},[137,44458,516],{"emptyLinePlaceholder":515},[137,44460,44461,44463,44465,44467,44469,44471,44473],{"class":139,"line":188},[137,44462,3077],{"class":143},[137,44464,40934],{"class":364},[137,44466,151],{"class":143},[137,44468,10468],{"class":147},[137,44470,356],{"class":157},[137,44472,6044],{"class":364},[137,44474,1502],{"class":157},[137,44476,44477],{"class":139,"line":269},[137,44478,516],{"emptyLinePlaceholder":515},[137,44480,44481,44483,44485,44488],{"class":139,"line":278},[137,44482,13456],{"class":143},[137,44484,154],{"class":143},[137,44486,44487],{"class":147}," useSharedCounter",[137,44489,275],{"class":157},[137,44491,44492,44494,44496,44498,44500,44502,44504,44506],{"class":139,"line":291},[137,44493,4177],{"class":143},[137,44495,41108],{"class":147},[137,44497,151],{"class":143},[137,44499,1484],{"class":157},[137,44501,222],{"class":143},[137,44503,44281],{"class":157},[137,44505,12683],{"class":143},[137,44507,3276],{"class":157},[137,44509,44510,44512,44515,44517,44519,44521,44523,44526],{"class":139,"line":297},[137,44511,4177],{"class":143},[137,44513,44514],{"class":147}," decrement",[137,44516,151],{"class":143},[137,44518,1484],{"class":157},[137,44520,222],{"class":143},[137,44522,44281],{"class":157},[137,44524,44525],{"class":143},"--",[137,44527,3276],{"class":157},[137,44529,44530,44532],{"class":139,"line":302},[137,44531,176],{"class":143},[137,44533,44534],{"class":157}," { count, increment, decrement };\n",[137,44536,44537],{"class":139,"line":662},[137,44538,510],{"class":157},[27,44540,44541],{},[22,44542,44543],{},"ComponentA.vue",[128,44545,44547],{"className":4024,"code":44546,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import { useSharedCounter } from \".\u002Fcomposables\u002FuseSharedCounter\";\n\n    const { count, increment } = useSharedCounter();\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv>\n        \u003Ch2>Component A\u003C\u002Fh2>\n        \u003Cbutton @click=\"increment\">Increment in A\u003C\u002Fbutton>\n        \u003Cp>Count: {{ count }}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,44548,44549,44559,44573,44577,44597,44605,44609,44617,44625,44638,44657,44669,44677],{"__ignoreMap":133},[137,44550,44551,44553,44555,44557],{"class":139,"line":140},[137,44552,4033],{"class":157},[137,44554,4037],{"class":4036},[137,44556,9642],{"class":147},[137,44558,4053],{"class":157},[137,44560,44561,44563,44566,44568,44571],{"class":139,"line":173},[137,44562,37677],{"class":143},[137,44564,44565],{"class":157}," { useSharedCounter } ",[137,44567,10954],{"class":143},[137,44569,44570],{"class":284}," \".\u002Fcomposables\u002FuseSharedCounter\"",[137,44572,3276],{"class":157},[137,44574,44575],{"class":139,"line":188},[137,44576,516],{"emptyLinePlaceholder":515},[137,44578,44579,44581,44583,44585,44587,44589,44591,44593,44595],{"class":139,"line":269},[137,44580,4177],{"class":143},[137,44582,8906],{"class":157},[137,44584,40296],{"class":364},[137,44586,164],{"class":157},[137,44588,44173],{"class":364},[137,44590,8911],{"class":157},[137,44592,253],{"class":143},[137,44594,44487],{"class":147},[137,44596,924],{"class":157},[137,44598,44599,44601,44603],{"class":139,"line":278},[137,44600,4083],{"class":157},[137,44602,4037],{"class":4036},[137,44604,4053],{"class":157},[137,44606,44607],{"class":139,"line":291},[137,44608,516],{"emptyLinePlaceholder":515},[137,44610,44611,44613,44615],{"class":139,"line":297},[137,44612,4033],{"class":157},[137,44614,7821],{"class":4036},[137,44616,4053],{"class":157},[137,44618,44619,44621,44623],{"class":139,"line":302},[137,44620,4072],{"class":157},[137,44622,8330],{"class":4036},[137,44624,4053],{"class":157},[137,44626,44627,44629,44631,44634,44636],{"class":139,"line":662},[137,44628,9826],{"class":157},[137,44630,104],{"class":4036},[137,44632,44633],{"class":157},">Component A\u003C\u002F",[137,44635,104],{"class":4036},[137,44637,4053],{"class":157},[137,44639,44640,44642,44644,44646,44648,44650,44653,44655],{"class":139,"line":667},[137,44641,9826],{"class":157},[137,44643,8170],{"class":4036},[137,44645,40994],{"class":147},[137,44647,253],{"class":157},[137,44649,40624],{"class":284},[137,44651,44652],{"class":157},">Increment in A\u003C\u002F",[137,44654,8170],{"class":4036},[137,44656,4053],{"class":157},[137,44658,44659,44661,44663,44665,44667],{"class":139,"line":786},[137,44660,9826],{"class":157},[137,44662,27],{"class":4036},[137,44664,40981],{"class":157},[137,44666,27],{"class":4036},[137,44668,4053],{"class":157},[137,44670,44671,44673,44675],{"class":139,"line":798},[137,44672,8374],{"class":157},[137,44674,8330],{"class":4036},[137,44676,4053],{"class":157},[137,44678,44679,44681,44683],{"class":139,"line":803},[137,44680,4083],{"class":157},[137,44682,7821],{"class":4036},[137,44684,4053],{"class":157},[27,44686,44687],{},[22,44688,44689],{},"ComponentB.vue",[128,44691,44693],{"className":4024,"code":44692,"language":4026,"meta":133,"style":133},"\u003Cscript setup>\n    import { useSharedCounter } from \".\u002Fcomposables\u002FuseSharedCounter\";\n\n    const { count, decrement } = useSharedCounter();\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv>\n        \u003Ch2>Component B\u003C\u002Fh2>\n        \u003Cbutton @click=\"decrement\">Decrement in B\u003C\u002Fbutton>\n        \u003Cp>Count: {{ count }}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,44694,44695,44705,44717,44721,44742,44750,44754,44762,44770,44783,44802,44814,44822],{"__ignoreMap":133},[137,44696,44697,44699,44701,44703],{"class":139,"line":140},[137,44698,4033],{"class":157},[137,44700,4037],{"class":4036},[137,44702,9642],{"class":147},[137,44704,4053],{"class":157},[137,44706,44707,44709,44711,44713,44715],{"class":139,"line":173},[137,44708,37677],{"class":143},[137,44710,44565],{"class":157},[137,44712,10954],{"class":143},[137,44714,44570],{"class":284},[137,44716,3276],{"class":157},[137,44718,44719],{"class":139,"line":188},[137,44720,516],{"emptyLinePlaceholder":515},[137,44722,44723,44725,44727,44729,44731,44734,44736,44738,44740],{"class":139,"line":269},[137,44724,4177],{"class":143},[137,44726,8906],{"class":157},[137,44728,40296],{"class":364},[137,44730,164],{"class":157},[137,44732,44733],{"class":364},"decrement",[137,44735,8911],{"class":157},[137,44737,253],{"class":143},[137,44739,44487],{"class":147},[137,44741,924],{"class":157},[137,44743,44744,44746,44748],{"class":139,"line":278},[137,44745,4083],{"class":157},[137,44747,4037],{"class":4036},[137,44749,4053],{"class":157},[137,44751,44752],{"class":139,"line":291},[137,44753,516],{"emptyLinePlaceholder":515},[137,44755,44756,44758,44760],{"class":139,"line":297},[137,44757,4033],{"class":157},[137,44759,7821],{"class":4036},[137,44761,4053],{"class":157},[137,44763,44764,44766,44768],{"class":139,"line":302},[137,44765,4072],{"class":157},[137,44767,8330],{"class":4036},[137,44769,4053],{"class":157},[137,44771,44772,44774,44776,44779,44781],{"class":139,"line":662},[137,44773,9826],{"class":157},[137,44775,104],{"class":4036},[137,44777,44778],{"class":157},">Component B\u003C\u002F",[137,44780,104],{"class":4036},[137,44782,4053],{"class":157},[137,44784,44785,44787,44789,44791,44793,44795,44798,44800],{"class":139,"line":667},[137,44786,9826],{"class":157},[137,44788,8170],{"class":4036},[137,44790,40994],{"class":147},[137,44792,253],{"class":157},[137,44794,40652],{"class":284},[137,44796,44797],{"class":157},">Decrement in B\u003C\u002F",[137,44799,8170],{"class":4036},[137,44801,4053],{"class":157},[137,44803,44804,44806,44808,44810,44812],{"class":139,"line":786},[137,44805,9826],{"class":157},[137,44807,27],{"class":4036},[137,44809,40981],{"class":157},[137,44811,27],{"class":4036},[137,44813,4053],{"class":157},[137,44815,44816,44818,44820],{"class":139,"line":798},[137,44817,8374],{"class":157},[137,44819,8330],{"class":4036},[137,44821,4053],{"class":157},[137,44823,44824,44826,44828],{"class":139,"line":803},[137,44825,4083],{"class":157},[137,44827,7821],{"class":4036},[137,44829,4053],{"class":157},[27,44831,44832,44833,114,44836,44839,44840,44842],{},"Both ",[22,44834,44835],{},"ComponentA",[22,44837,44838],{},"ComponentB"," in your app, they will share the same ",[22,44841,40296],{}," state, and any changes in one will be reflected in the other.",[104,44844,44845],{"id":2566},[42,44846,2567],{},[27,44848,44849],{},"Vue and React both excel in building modern web applications, but they take different paths. Vue's declarative syntax, reactivity, and lifecycle hooks make it approachable for beginners, at least in my experience. Having spent considerable time with both frameworks, I believe that while their approaches differ, they share core concepts and offer similar features.",[27,44851,44852],{},"This guide only scratches the surface. More advanced features like Vue Router and state management with Pinia are beyond the scope of this beginner's guide, but you can explore them at your own pace.",[27,44854,44855],{},"Happy coding!",[27,44857,36698,44858,1017],{},[45,44859,10647],{"href":44860,"target":2716,"rel":44861},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fa-beginners-guide-to-vue-for-react-developers",[2718,2719],[2617,44863,44864],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":133,"searchDepth":173,"depth":173,"links":44866},[44867,44871,44882,44886,44891,44895,44899,44903,44907,44911,44915,44919,44924],{"id":39272,"depth":173,"text":39273,"children":44868},[44869,44870],{"id":39276,"depth":188,"text":36801},{"id":27822,"depth":188,"text":26585},{"id":39414,"depth":173,"text":39415,"children":44872},[44873,44874,44875,44876,44877,44878,44879,44880],{"id":39421,"depth":188,"text":39424},{"id":39430,"depth":188,"text":36801},{"id":39509,"depth":188,"text":26585},{"id":39602,"depth":188,"text":39603},{"id":39609,"depth":188,"text":36801},{"id":39731,"depth":188,"text":26585},{"id":39873,"depth":188,"text":39874},{"id":40011,"depth":188,"text":44881},"React's Approach to Scoped Styles",{"id":40246,"depth":173,"text":40247,"children":44883},[44884,44885],{"id":40250,"depth":188,"text":36801},{"id":40889,"depth":188,"text":26585},{"id":41524,"depth":173,"text":44887,"children":44888},"Two-Way Binding with v-model",[44889,44890],{"id":41536,"depth":188,"text":36801},{"id":41730,"depth":188,"text":26585},{"id":41873,"depth":173,"text":41874,"children":44892},[44893,44894],{"id":41877,"depth":188,"text":36801},{"id":42010,"depth":188,"text":26585},{"id":42164,"depth":173,"text":42165,"children":44896},[44897,44898],{"id":42168,"depth":188,"text":36801},{"id":42300,"depth":188,"text":26585},{"id":42452,"depth":173,"text":42453,"children":44900},[44901,44902],{"id":42456,"depth":188,"text":36801},{"id":42524,"depth":188,"text":26585},{"id":42640,"depth":173,"text":42641,"children":44904},[44905,44906],{"id":42644,"depth":188,"text":36801},{"id":42766,"depth":188,"text":26585},{"id":42949,"depth":173,"text":42950,"children":44908},[44909,44910],{"id":42953,"depth":188,"text":36801},{"id":43059,"depth":188,"text":26585},{"id":43405,"depth":173,"text":43406,"children":44912},[44913,44914],{"id":43409,"depth":188,"text":36801},{"id":43509,"depth":188,"text":26585},{"id":43723,"depth":173,"text":43724,"children":44916},[44917,44918],{"id":43727,"depth":188,"text":36801},{"id":43885,"depth":188,"text":26585},{"id":44070,"depth":173,"text":44071,"children":44920},[44921,44922,44923],{"id":44074,"depth":188,"text":36801},{"id":44211,"depth":188,"text":26585},{"id":44425,"depth":188,"text":44426},{"id":2566,"depth":173,"text":2567},"A comprehensive guide for React developers transitioning to Vue. Learn the differences in components, styling, state management, lifecycle hooks, and more. Master Vue concepts quickly with React comparisons. We will dive into Vue's unique features, like the ref and reactive functions, v-if directives, and computed properties, make it a powerful alternative to React. Whether you're a React developer exploring Vue or simply curious about its differences, this guide provides practical examples and insights to get you started.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1736935516\u002Fblog\u002Fa-beginners-guide-to-vue-for-react-developers\u002FA_scenic_landscape_featuring_a_slightly_curved_road_that_extends_infinitely_into_the_horizon._The_road_starts_with_a_vibrant_blue_hue_and_gradually_tr",[29177,8,44928,44929,36722,8448,41528,5300,5299,36724,36726,36728,36729,44930,44931,44932,44933,44934,44935,44936,44937,44938,44939,44940,44941,44942,44943,44944,44945,44946,44947,44948,44949,44950,44951],"Reactivity in Vue.js","Reactivity in React.js","Vue for React developers","React to Vue transition","Vue beginner guide","React vs Vue comparison","Vue vs React components","Vue state management","Vue lifecycle hooks","Vue reactivity","Vue templates","Vue scoped styles","Vue conditional rendering","Vue computed properties","React vs Vue props","Vue slots","React CSS Modules","Vue styled-components alternative","Vue frontend framework","Vue directive examples","Vue reactive variables","Vue ref vs React useState","Vue hooks","transitioning from React to Vue",{},"\u002F2025\u002F01\u002F15\u002Fa-beginners-guide-to-vue-for-react-developers","15th January 2025",{"title":39235,"description":44925},"2025\u002F01\u002F15\u002Fa-beginners-guide-to-vue-for-react-developers","RFQ4J9WSFnjVjjnXxPUm8jhk0Rdum9bQrlSivaTA3wA",{"id":44959,"title":44960,"articleTags":44961,"author":11,"blog":12,"body":44962,"description":45947,"extension":2649,"image":45948,"keywords":45949,"meta":45971,"navigation":515,"path":45972,"published":45973,"readTime":291,"seo":45974,"stem":45975,"type":2662,"__hash__":45976},"content\u002F2025\u002F03\u002F02\u002Fhow-to-bind-props-in-vue-correctly.md","How to Bind Props in Vue Correctly",[10,8,9],{"type":14,"value":44963,"toc":45933},[44964,44967,44981,44983,44987,44992,44995,44998,45002,45009,45069,45073,45081,45085,45091,45113,45119,45140,45146,45165,45178,45182,45186,45206,45210,45229,45248,45263,45269,45274,45294,45299,45318,45329,45333,45337,45357,45361,45381,45385,45416,45434,45444,45448,45452,45472,45476,45496,45500,45520,45525,45535,45541,45545,45565,45569,45588,45596,45602,45606,45626,45630,45649,45657,45663,45667,45686,45690,45710,45714,45733,45739,45930],[17,44965,44960],{"id":44966},"how-to-bind-props-in-vue-correctly",[27,44968,44969],{},[30,44970,44971,36,44973,40,44975],{},[33,44972],{"value":35},[33,44974],{"value":39},[42,44976,44977],{},[45,44978,44979],{"href":47},[33,44980],{"value":50},[52,44982],{":tags":54},[56,44984],{":audio-src":44985,":transcript-src":44986},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F03\u002F02\u002Fhow-to-bind-props-in-vue-correctly\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F03\u002F02\u002Fhow-to-bind-props-in-vue-correctly\u002Fsummary.json",[27,44988,44989],{},[63,44990],{"alt":12847,"src":44991},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1740813459\u002Fblog\u002Fhow-to-bind-props-in-vue-correctly\u002Fbuilding_blocks_of_vue_3d_representation_of_1_wsoaxn",[27,44993,44994],{},"Vue makes data binding in props simple and intuitive, but small syntax errors, especially when coming from other frameworks or languages, can lead to unexpected behaviour.",[27,44996,44997],{},"In this article, we'll break down the correct, unnecessary, and incorrect ways to bind props in Vue components when using them in the template.",[104,44999,45001],{"id":45000},"understanding-vue-prop-types","Understanding Vue Prop Types",[27,45003,45004,45005,45008],{},"Before diving into prop binding, it's essential to understand the different ",[42,45006,45007],{},"prop types"," that Vue supports:",[1003,45010,45011,45020,45029,45042,45051,45060],{},[1006,45012,45013,45019],{},[42,45014,45015,45016,14105],{},"String (",[22,45017,45018],{},"String",": Accepts text values.",[1006,45021,45022,45028],{},[42,45023,45024,45025,14105],{},"Number (",[22,45026,45027],{},"Number",": Accepts numeric values.",[1006,45030,45031,45037,45038,3955,45040,1017],{},[42,45032,45033,45034,14105],{},"Boolean (",[22,45035,45036],{},"Boolean",": Accepts ",[22,45039,3097],{},[22,45041,30105],{},[1006,45043,45044,45050],{},[42,45045,45046,45047,14105],{},"Object (",[22,45048,45049],{},"Object",": Accepts objects.",[1006,45052,45053,45059],{},[42,45054,45055,45056,14105],{},"Array (",[22,45057,45058],{},"Array",": Accepts arrays.",[1006,45061,45062,45068],{},[42,45063,45064,45065,14105],{},"Function (",[22,45066,45067],{},"Function",": Accepts functions.",[104,45070,45072],{"id":45071},"simple-vue-bindings","Simple Vue Bindings",[27,45074,39860,45075,45077,45078,45080],{},[22,45076,7568],{}," (or shorthand ",[22,45079,894],{},") to bind data dynamically to props. However, different types of props require different approaches. Let’s explore them with practical examples.",[123,45082,45084],{"id":45083},"_1-static-props","1. Static Props",[27,45086,45087,45088],{},"✅ ",[42,45089,45090],{},"Correct:",[128,45092,45094],{"className":4024,"code":45093,"language":4026,"meta":133,"style":133},"\u003CComponent prop=\"Text\" \u002F>\n",[22,45095,45096],{"__ignoreMap":133},[137,45097,45098,45100,45103,45106,45108,45111],{"class":139,"line":140},[137,45099,4033],{"class":157},[137,45101,45102],{"class":8180},"Component",[137,45104,45105],{"class":147}," prop",[137,45107,253],{"class":157},[137,45109,45110],{"class":284},"\"Text\"",[137,45112,4078],{"class":157},[27,45114,45115,45116],{},"⚠️ ",[42,45117,45118],{},"Unnecessary:",[128,45120,45122],{"className":4024,"code":45121,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"'Text'\" \u002F>\n",[22,45123,45124],{"__ignoreMap":133},[137,45125,45126,45128,45130,45133,45135,45138],{"class":139,"line":140},[137,45127,4033],{"class":157},[137,45129,45102],{"class":8180},[137,45131,45132],{"class":147}," :prop",[137,45134,253],{"class":157},[137,45136,45137],{"class":284},"\"'Text'\"",[137,45139,4078],{"class":157},[27,45141,45142,45143],{},"❌ ",[42,45144,45145],{},"Incorrect:",[128,45147,45149],{"className":4024,"code":45148,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"Text\" \u002F>\n",[22,45150,45151],{"__ignoreMap":133},[137,45152,45153,45155,45157,45159,45161,45163],{"class":139,"line":140},[137,45154,4033],{"class":157},[137,45156,45102],{"class":8180},[137,45158,45132],{"class":147},[137,45160,253],{"class":157},[137,45162,45110],{"class":284},[137,45164,4078],{"class":157},[27,45166,45167,45170,45171,45173,45174,45177],{},[42,45168,45169],{},"Explanation:"," Static props should be written as plain strings without ",[22,45172,894],{}," since they don't need dynamic binding. The unnecessary syntax binds a static string dynamically, which isn’t needed. The incorrect syntax lacks quotes around ",[22,45175,45176],{},"Text",", making Vue treat it as a variable instead of a string.",[123,45179,45181],{"id":45180},"_2-boolean-props","2. Boolean Props",[27,45183,45087,45184],{},[42,45185,45090],{},[128,45187,45189],{"className":4024,"code":45188,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"true\" \u002F>\n",[22,45190,45191],{"__ignoreMap":133},[137,45192,45193,45195,45197,45199,45201,45204],{"class":139,"line":140},[137,45194,4033],{"class":157},[137,45196,45102],{"class":8180},[137,45198,45132],{"class":147},[137,45200,253],{"class":157},[137,45202,45203],{"class":284},"\"true\"",[137,45205,4078],{"class":157},[27,45207,45142,45208],{},[42,45209,45145],{},[128,45211,45213],{"className":4024,"code":45212,"language":4026,"meta":133,"style":133},"\u003CComponent prop=\"true\" \u002F>\n",[22,45214,45215],{"__ignoreMap":133},[137,45216,45217,45219,45221,45223,45225,45227],{"class":139,"line":140},[137,45218,4033],{"class":157},[137,45220,45102],{"class":8180},[137,45222,45105],{"class":147},[137,45224,253],{"class":157},[137,45226,45203],{"class":284},[137,45228,4078],{"class":157},[27,45230,45231,45233,45234,45236,45237,3955,45239,45241,45242,45244,45245,45247],{},[42,45232,45169],{}," Boolean props require ",[22,45235,894],{}," to be properly recognized as ",[22,45238,3097],{},[22,45240,30105],{},". Without ",[22,45243,894],{},", the value is treated as a string (",[22,45246,45203],{},"), which may not behave as expected.",[3244,45249,45250],{},[27,45251,45252,45253,45256,45257,45260,45261,1017],{},"💡 ",[42,45254,45255],{},"Tip:"," In Vue, simply adding a boolean prop without a value (e.g., ",[22,45258,45259],{},"\u003CComponent prop \u002F>",") is equivalent to passing ",[22,45262,3097],{},[123,45264,45266],{"id":45265},"_3-binding-numbers-in-props",[42,45267,45268],{},"3. Binding Numbers in Props",[27,45270,45271,45272],{},"✅ ",[42,45273,45090],{},[128,45275,45277],{"className":4024,"code":45276,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"42\" \u002F>\n",[22,45278,45279],{"__ignoreMap":133},[137,45280,45281,45283,45285,45287,45289,45292],{"class":139,"line":140},[137,45282,4033],{"class":157},[137,45284,45102],{"class":8180},[137,45286,45132],{"class":147},[137,45288,253],{"class":157},[137,45290,45291],{"class":284},"\"42\"",[137,45293,4078],{"class":157},[27,45295,45296,45297],{},"❌ ",[42,45298,45145],{},[128,45300,45302],{"className":4024,"code":45301,"language":4026,"meta":133,"style":133},"\u003CComponent prop=\"42\" \u002F>\n",[22,45303,45304],{"__ignoreMap":133},[137,45305,45306,45308,45310,45312,45314,45316],{"class":139,"line":140},[137,45307,4033],{"class":157},[137,45309,45102],{"class":8180},[137,45311,45105],{"class":147},[137,45313,253],{"class":157},[137,45315,45291],{"class":284},[137,45317,4078],{"class":157},[27,45319,45320,45322,45323,45325,45326,45328],{},[42,45321,45169],{}," Numbers should always be bound using ",[22,45324,894],{}," to ensure they are treated as actual numeric values. Without ",[22,45327,894],{},", Vue treats them as strings, which can lead to unexpected type issues in the component.",[123,45330,45332],{"id":45331},"_4-dynamic-props","4. Dynamic Props",[27,45334,45087,45335],{},[42,45336,45090],{},[128,45338,45340],{"className":4024,"code":45339,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"item.name\" \u002F>\n",[22,45341,45342],{"__ignoreMap":133},[137,45343,45344,45346,45348,45350,45352,45355],{"class":139,"line":140},[137,45345,4033],{"class":157},[137,45347,45102],{"class":8180},[137,45349,45132],{"class":147},[137,45351,253],{"class":157},[137,45353,45354],{"class":284},"\"item.name\"",[137,45356,4078],{"class":157},[27,45358,45115,45359],{},[42,45360,45118],{},[128,45362,45364],{"className":4024,"code":45363,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"`${item.name}`\" \u002F>\n",[22,45365,45366],{"__ignoreMap":133},[137,45367,45368,45370,45372,45374,45376,45379],{"class":139,"line":140},[137,45369,4033],{"class":157},[137,45371,45102],{"class":8180},[137,45373,45132],{"class":147},[137,45375,253],{"class":157},[137,45377,45378],{"class":284},"\"`${item.name}`\"",[137,45380,4078],{"class":157},[27,45382,45142,45383],{},[42,45384,45145],{},[128,45386,45388],{"className":4024,"code":45387,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"{{ item.name }}\" \u002F> \u003CComponent prop=\"item.name\" \u002F>\n",[22,45389,45390],{"__ignoreMap":133},[137,45391,45392,45394,45396,45398,45400,45403,45406,45408,45410,45412,45414],{"class":139,"line":140},[137,45393,4033],{"class":157},[137,45395,45102],{"class":8180},[137,45397,45132],{"class":147},[137,45399,253],{"class":157},[137,45401,45402],{"class":284},"\"{{ item.name }}\"",[137,45404,45405],{"class":157}," \u002F> \u003C",[137,45407,45102],{"class":8180},[137,45409,45105],{"class":147},[137,45411,253],{"class":157},[137,45413,45354],{"class":284},[137,45415,4078],{"class":157},[27,45417,45418,45420,45421,45423,45424,45427,45428,45430,45431,45433],{},[42,45419,45169],{}," To pass a dynamic value, ",[22,45422,894],{}," is required to bind ",[22,45425,45426],{},"item.name"," properly. The unnecessary syntax wraps the value in a template literal, which is redundant. The incorrect examples either incorrectly use ",[22,45429,41510],{}," (which is for templates, not bindings) or treat ",[22,45432,45354],{}," as a static string instead of referencing the variable.",[3244,45435,45436],{},[27,45437,45252,45438,45440,45441,45443],{},[42,45439,45255],{}," The ",[22,45442,894],{}," shorthand tells Vue to evaluate an expression instead of treating it as a string.",[123,45445,45447],{"id":45446},"_5-props-with-template-strings","5. Props with Template Strings",[27,45449,45087,45450],{},[42,45451,45090],{},[128,45453,45455],{"className":4024,"code":45454,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"`Go to ${item.name}`\" \u002F>\n",[22,45456,45457],{"__ignoreMap":133},[137,45458,45459,45461,45463,45465,45467,45470],{"class":139,"line":140},[137,45460,4033],{"class":157},[137,45462,45102],{"class":8180},[137,45464,45132],{"class":147},[137,45466,253],{"class":157},[137,45468,45469],{"class":284},"\"`Go to ${item.name}`\"",[137,45471,4078],{"class":157},[27,45473,45115,45474],{},[42,45475,45118],{},[128,45477,45479],{"className":4024,"code":45478,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"'Go to ' + item.name\" \u002F>\n",[22,45480,45481],{"__ignoreMap":133},[137,45482,45483,45485,45487,45489,45491,45494],{"class":139,"line":140},[137,45484,4033],{"class":157},[137,45486,45102],{"class":8180},[137,45488,45132],{"class":147},[137,45490,253],{"class":157},[137,45492,45493],{"class":284},"\"'Go to ' + item.name\"",[137,45495,4078],{"class":157},[27,45497,45142,45498],{},[42,45499,45145],{},[128,45501,45503],{"className":4024,"code":45502,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"Go to ${item.name}\" \u002F>\n",[22,45504,45505],{"__ignoreMap":133},[137,45506,45507,45509,45511,45513,45515,45518],{"class":139,"line":140},[137,45508,4033],{"class":157},[137,45510,45102],{"class":8180},[137,45512,45132],{"class":147},[137,45514,253],{"class":157},[137,45516,45517],{"class":284},"\"Go to ${item.name}\"",[137,45519,4078],{"class":157},[27,45521,45522,45524],{},[42,45523,45169],{}," The best approach is using template literals with backticks for readability. The unnecessary syntax still works but concatenates strings in a less readable way. The incorrect syntax lacks quotes, making Vue crash because it expects a dynamic variable.",[3244,45526,45527],{},[27,45528,45252,45529,45531,45532,45534],{},[42,45530,45255],{}," Template literals (``) improve readability, support multi-line strings, and eliminate the need for explicit ",[22,45533,182],{}," concatenation.",[123,45536,45538],{"id":45537},"_6-passing-objects-as-props",[42,45539,45540],{},"6. Passing Objects as Props",[27,45542,45271,45543],{},[42,45544,45090],{},[128,45546,45548],{"className":4024,"code":45547,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"{ name: 'Vue', version: 3 }\" \u002F>\n",[22,45549,45550],{"__ignoreMap":133},[137,45551,45552,45554,45556,45558,45560,45563],{"class":139,"line":140},[137,45553,4033],{"class":157},[137,45555,45102],{"class":8180},[137,45557,45132],{"class":147},[137,45559,253],{"class":157},[137,45561,45562],{"class":284},"\"{ name: 'Vue', version: 3 }\"",[137,45564,4078],{"class":157},[27,45566,45296,45567],{},[42,45568,45145],{},[128,45570,45572],{"className":4024,"code":45571,"language":4026,"meta":133,"style":133},"\u003CComponent prop=\"{ name: 'Vue', version: 3 }\" \u002F>\n",[22,45573,45574],{"__ignoreMap":133},[137,45575,45576,45578,45580,45582,45584,45586],{"class":139,"line":140},[137,45577,4033],{"class":157},[137,45579,45102],{"class":8180},[137,45581,45105],{"class":147},[137,45583,253],{"class":157},[137,45585,45562],{"class":284},[137,45587,4078],{"class":157},[27,45589,45590,45592,45593,45595],{},[42,45591,45169],{}," Objects should always be bound dynamically using ",[22,45594,894],{}," to be correctly parsed as JavaScript objects.",[123,45597,45599],{"id":45598},"_7-passing-arrays-as-props",[42,45600,45601],{},"7. Passing Arrays as Props",[27,45603,45271,45604],{},[42,45605,45090],{},[128,45607,45609],{"className":4024,"code":45608,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"[1, 2, 3]\" \u002F>\n",[22,45610,45611],{"__ignoreMap":133},[137,45612,45613,45615,45617,45619,45621,45624],{"class":139,"line":140},[137,45614,4033],{"class":157},[137,45616,45102],{"class":8180},[137,45618,45132],{"class":147},[137,45620,253],{"class":157},[137,45622,45623],{"class":284},"\"[1, 2, 3]\"",[137,45625,4078],{"class":157},[27,45627,45296,45628],{},[42,45629,45145],{},[128,45631,45633],{"className":4024,"code":45632,"language":4026,"meta":133,"style":133},"\u003CComponent prop=\"[1, 2, 3]\" \u002F>\n",[22,45634,45635],{"__ignoreMap":133},[137,45636,45637,45639,45641,45643,45645,45647],{"class":139,"line":140},[137,45638,4033],{"class":157},[137,45640,45102],{"class":8180},[137,45642,45105],{"class":147},[137,45644,253],{"class":157},[137,45646,45623],{"class":284},[137,45648,4078],{"class":157},[27,45650,45651,45653,45654,45656],{},[42,45652,45169],{}," Arrays must be bound using ",[22,45655,894],{}," to ensure they are treated as JavaScript arrays and not strings.",[123,45658,45660],{"id":45659},"_8-passing-functions-to-props",[42,45661,45662],{},"8. Passing Functions to Props",[27,45664,45271,45665],{},[42,45666,45090],{},[128,45668,45670],{"className":4024,"code":45669,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"handleClick\" \u002F>\n",[22,45671,45672],{"__ignoreMap":133},[137,45673,45674,45676,45678,45680,45682,45684],{"class":139,"line":140},[137,45675,4033],{"class":157},[137,45677,45102],{"class":8180},[137,45679,45132],{"class":147},[137,45681,253],{"class":157},[137,45683,43699],{"class":284},[137,45685,4078],{"class":157},[27,45687,45296,45688],{},[42,45689,45145],{},[128,45691,45693],{"className":4024,"code":45692,"language":4026,"meta":133,"style":133},"\u003CComponent :prop=\"handleClick()\" \u002F>\n",[22,45694,45695],{"__ignoreMap":133},[137,45696,45697,45699,45701,45703,45705,45708],{"class":139,"line":140},[137,45698,4033],{"class":157},[137,45700,45102],{"class":8180},[137,45702,45132],{"class":147},[137,45704,253],{"class":157},[137,45706,45707],{"class":284},"\"handleClick()\"",[137,45709,4078],{"class":157},[27,45711,45712],{},[42,45713,45169],{},[1003,45715,45716,45723],{},[1006,45717,45718,45719,45722],{},"The correct approach passes the function ",[22,45720,45721],{},"handleClick"," as a reference, allowing Vue to call it when needed.",[1006,45724,45725,45726,3596,45729,45732],{},"The incorrect approach ",[42,45727,45728],{},"executes",[22,45730,45731],{},"handleClick()"," immediately when rendering, which is not the intended behaviour.",[104,45734,45736],{"id":45735},"cheat-sheet",[42,45737,45738],{},"Cheat Sheet",[45740,45741,45742,45761],"table",{},[45743,45744,45745],"thead",{},[45746,45747,45748,45752,45755,45758],"tr",{},[45749,45750,45751],"th",{},"Use Case",[45749,45753,45754],{},"✅ Correct Syntax",[45749,45756,45757],{},"⚠️ Unnecessary Syntax",[45749,45759,45760],{},"❌ Incorrect Syntax",[45762,45763,45764,45785,45804,45821,45855,45879,45896,45913],"tbody",{},[45746,45765,45766,45770,45775,45780],{},[45767,45768,45769],"td",{},"Static Props",[45767,45771,45772],{},[22,45773,45774],{},"\u003CComponent prop=\"Text\"\u002F>",[45767,45776,45777],{},[22,45778,45779],{},"\u003CComponent :prop=\"'Text'\"\u002F>",[45767,45781,45782],{},[22,45783,45784],{},"\u003CComponent :prop=\"Text\"\u002F>",[45746,45786,45787,45790,45794,45799],{},[45767,45788,45789],{},"Boolean Props",[45767,45791,45792],{},[22,45793,45259],{},[45767,45795,45796],{},[22,45797,45798],{},"\u003CComponent :prop=\"true\"\u002F>",[45767,45800,45801],{},[22,45802,45803],{},"\u003CComponent prop=\"true\"\u002F>",[45746,45805,45806,45809,45814,45816],{},[45767,45807,45808],{},"Number Props",[45767,45810,45811],{},[22,45812,45813],{},"\u003CComponent :prop=\"42\"\u002F>",[45767,45815],{},[45767,45817,45818],{},[22,45819,45820],{},"\u003CComponent prop=\"42\"\u002F>",[45746,45822,45823,45826,45831,45842],{},[45767,45824,45825],{},"Dynamic Props",[45767,45827,45828],{},[22,45829,45830],{},"\u003CComponent :prop=\"item.name\"\u002F>",[45767,45832,45833,22056,45836,22056,45839],{},[22,45834,45835],{},"\u003CComponent :prop=\"",[22,45837,45838],{},"${item.name}",[22,45840,45841],{},"\"\u002F>",[45767,45843,45844,45847,3596,45850,3596,45852],{},[22,45845,45846],{},"\u003CComponent :prop=\"{{ item.name }}\"\u002F>",[45848,45849],"br",{},[45848,45851],{},[22,45853,45854],{},"\u003CComponent prop=\"item.name\"\u002F>",[45746,45856,45857,45860,45869,45874],{},[45767,45858,45859],{},"Props with Template Strings",[45767,45861,45862,22056,45864,22056,45867],{},[22,45863,45835],{},[22,45865,45866],{},"Go to ${item.name}",[22,45868,45841],{},[45767,45870,45871],{},[22,45872,45873],{},"\u003CComponent :prop=\"'Go to ' + item.name\"\u002F>",[45767,45875,45876],{},[22,45877,45878],{},"\u003CComponent :prop=\"Go to ${item.name}\"\u002F>",[45746,45880,45881,45884,45889,45891],{},[45767,45882,45883],{},"Passing Objects as Props",[45767,45885,45886],{},[22,45887,45888],{},"\u003CComponent :prop=\"{ name: 'Vue', version: 3 }\" \u002F>",[45767,45890],{},[45767,45892,45893],{},[22,45894,45895],{},"\u003CComponent prop=\"{ name: 'Vue', version: 3 }\" \u002F>",[45746,45897,45898,45901,45906,45908],{},[45767,45899,45900],{},"Passing Arrays as Props",[45767,45902,45903],{},[22,45904,45905],{},"\u003CComponent :prop=\"[1, 2, 3]\" \u002F>",[45767,45907],{},[45767,45909,45910],{},[22,45911,45912],{},"\u003CComponent prop=\"[1, 2, 3]\" \u002F>",[45746,45914,45915,45918,45923,45925],{},[45767,45916,45917],{},"Passing Functions to Props",[45767,45919,45920],{},[22,45921,45922],{},"\u003CComponent :prop=\"handleClick\" \u002F>",[45767,45924],{},[45767,45926,45927],{},[22,45928,45929],{},"\u003CComponent :prop=\"handleClick()\" \u002F>",[2617,45931,45932],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":133,"searchDepth":173,"depth":173,"links":45934},[45935,45936,45946],{"id":45000,"depth":173,"text":45001},{"id":45071,"depth":173,"text":45072,"children":45937},[45938,45939,45940,45941,45942,45943,45944,45945],{"id":45083,"depth":188,"text":45084},{"id":45180,"depth":188,"text":45181},{"id":45265,"depth":188,"text":45268},{"id":45331,"depth":188,"text":45332},{"id":45446,"depth":188,"text":45447},{"id":45537,"depth":188,"text":45540},{"id":45598,"depth":188,"text":45601},{"id":45659,"depth":188,"text":45662},{"id":45735,"depth":173,"text":45738},"Master Vue props binding with this in-depth guide! Learn how to correctly pass strings, numbers, booleans, objects, arrays, and functions as props. Avoid common mistakes, understand prop types, and follow best practices to write clean, efficient, and error-free Vue components.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1740813459\u002Fblog\u002Fhow-to-bind-props-in-vue-correctly\u002Fbuilding_blocks_of_vue_3d_representation_of_1_wsoaxn",[45950,45951,45952,45953,45954,45955,45956,45957,45958,45959,45960,45961,45962,45963,45964,45965,45966,45967,45968,45969,45970],"Vue props binding","Vue component props","Vue props tutorial","Passing props in Vue","Vue 3 props","Vue props best practices","Vue dynamic props","Vue boolean props","Vue function props","Vue number props","Vue object props","Vue array props","Vue template literals props","Vue props validation","Vue defineProps","Vue props types","How to pass props in Vue 3","Best way to bind props in Vue","Common mistakes in Vue props binding","Difference between Vue 2 and Vue 3 props","How to pass functions as props in Vue",{},"\u002F2025\u002F03\u002F02\u002Fhow-to-bind-props-in-vue-correctly","2nd March 2025",{"title":44960,"description":45947},"2025\u002F03\u002F02\u002Fhow-to-bind-props-in-vue-correctly","2aVcyyaFuDmgLAluQB3mhF97krUN7cH1p19BUCF4Kzw",{"id":45978,"title":45979,"articleTags":45980,"author":11,"blog":12,"body":45981,"description":47663,"extension":2649,"image":47664,"keywords":47665,"meta":47674,"navigation":515,"path":47675,"published":47676,"readTime":291,"seo":47677,"stem":47678,"type":2662,"__hash__":47679},"content\u002F2025\u002F03\u002F24\u002Fcreate-a-recommendation-engine-with-ai-for-free.md","Create a Recommendation Engine with AI for Free",[27886,2669,9],{"type":14,"value":45982,"toc":47644},[45983,45986,46000,46002,46006,46011,46014,46017,46023,46026,46029,46033,46036,46043,46091,46097,46100,46104,46107,46111,46114,46126,46138,46147,46150,46156,46163,46166,46170,46173,46176,46179,46204,46208,46211,46215,46223,46226,46230,46235,46242,46253,46257,46264,46278,46281,46403,46406,47121,47124,47171,47175,47178,47185,47199,47203,47398,47402,47555,47559,47562,47565,47584,47593,47597,47600,47603,47606,47609,47621,47627,47634,47641],[17,45984,45979],{"id":45985},"create-a-recommendation-engine-with-ai-for-free",[27,45987,45988],{},[30,45989,45990,36,45992,40,45994],{},[33,45991],{"value":35},[33,45993],{"value":39},[42,45995,45996],{},[45,45997,45998],{"href":47},[33,45999],{"value":50},[52,46001],{":tags":54},[56,46003],{":audio-src":46004,":transcript-src":46005},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F03\u002F24\u002Fcreate-a-recommendation-engine-with-ai-for-free\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F03\u002F24\u002Fcreate-a-recommendation-engine-with-ai-for-free\u002Fsummary.json",[27,46007,46008],{},[63,46009],{"alt":12847,"src":46010},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1774661369\u002Fblog\u002Fcreate-a-recommendation-engine-with-ai-for-free\u002Fcreate_a_recommendation_engine_with_ai_for_free_slimzx",[27,46012,46013],{},"Recently, I had some free time since I was taking a month off work for the arrival of my daughter. While at home, I decided to make some updates to my personal blog.",[27,46015,46016],{},"Previously, my blog displayed three recommended articles at the bottom of each post. These articles were simply the next consecutive ones, which was easy to implement and required no AI or complex algorithms. For example, if an article was written on March 2, 2025, the recommended articles would be from January 15, January 9, and January 4, since these were the three most recently published articles before the current one. See image below",[27,46018,46019],{},[63,46020],{"alt":46021,"src":46022},"Image One","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1742804593\u002Fblog\u002Fcreate-a-recommendation-engine-with-ai-for-free\u002Fimage-1_dtbwt2",[27,46024,46025],{},"However, these recommendations weren't very relevant - readers rarely found articles that matched their interests.",[27,46027,46028],{},"In this article, I'll walk you through exactly how I did it - for free - using open-source AI models to make article suggestions more relevant for readers. Let's dive in!",[104,46030,46032],{"id":46031},"what-are-embedding-models-and-why-do-we-need-them","What Are Embedding Models and Why Do We Need Them?",[27,46034,46035],{},"Before we dive into the implementation, let's first understand embedding models and why they are essential for building a recommendation system.",[27,46037,46038,46039,46042],{},"At a high level, embedding models are AI models that convert text into ",[42,46040,46041],{},"numerical vectors"," - essentially transforming words, sentences, or even entire articles into numbers that a computer can understand. These vectors help capture the meaning and relationships between different pieces of text.",[27,46044,46045,46046,164,46049,164,46052,14528,46055,46058,46059,987,46061,164,46064,3596,46066,164,46069,3596,46071,14528,46074,3596,46076,46079,46080,114,46082,46084,46085,46087,46088,46090],{},"For example, imagine we want to represent words in a 2D space where similar words are close together. An embedding model can do this. Take the words ",[22,46047,46048],{},"cat",[22,46050,46051],{},"dog",[22,46053,46054],{},"fish",[22,46056,46057],{},"sun",". We can represent them with vectors like this: ",[22,46060,46048],{},[22,46062,46063],{},"[2, 3]",[22,46065,46051],{},[22,46067,46068],{},"[3, 4]",[22,46070,46054],{},[22,46072,46073],{},"[6, 2]",[22,46075,46057],{},[22,46077,46078],{},"[8, 9]",". Here, ",[22,46081,46048],{},[22,46083,46051],{}," are close because they are both animals, while ",[22,46086,46054],{}," is further away (another animal but different category), and ",[22,46089,46057],{}," is very far because it's unrelated.",[27,46092,46093],{},[63,46094],{"alt":46095,"src":46096},"Image Two","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1742805960\u002Fblog\u002Fcreate-a-recommendation-engine-with-ai-for-free\u002Fimage_dygz3c",[27,46098,46099],{},"In the context of our recommendation system, embeddings allow us to measure the similarity between articles. Instead of just suggesting the next article in order, we can recommend articles that are most relevant to what the reader is currently viewing.",[104,46101,46103],{"id":46102},"paid-vs-free-embedding-models","Paid vs. Free Embedding Models",[27,46105,46106],{},"There are many different embedding models available, and they generally fall into two categories:",[123,46108,46110],{"id":46109},"paid-embedding-models-eg-openai-voyageai-google","Paid Embedding Models (e.g., OpenAI, VoyageAI, Google)",[27,46112,46113],{},"The major AI companies like OpenAI, VoyageAI, and Google provide embedding models.",[27,46115,46116,46117,164,46120,14528,46123,1017],{},"OpenAI offers high-quality embedding models, such as ",[22,46118,46119],{},"text-embedding-3-large",[22,46121,46122],{},"text-embedding-3-small",[22,46124,46125],{},"text-embedding-ada-002",[27,46127,46128,46129,164,46132,14528,46135,1017],{},"VoyageAI's options include ",[22,46130,46131],{},"voyage-3",[22,46133,46134],{},"voyage-3-large",[22,46136,46137],{},"voyage-3-lite",[27,46139,46140,46141,114,46144,1017],{},"Google provides ",[22,46142,46143],{},"text-embedding-004",[22,46145,46146],{},"text-embedding-005",[27,46148,46149],{},"Since these models charge API usage fees, they aren't ideal for small projects or personal blogs.",[123,46151,46153],{"id":46152},"free-embedding-models-eg-hugging-face",[42,46154,46155],{},"Free Embedding Models (e.g., Hugging Face)",[27,46157,46158,46159,46162],{},"Fortunately, there are open-source alternatives that are completely free! ",[45,46160,27992],{"href":27990,"target":2716,"rel":46161},[2718,2719]," is a popular platform that hosts a wide range of pre-trained AI models, including embedding models. Many of these models can be used without any cost, making them perfect for personal projects like ours.",[27,46164,46165],{},"For this tutorial, we will use a free embedding model from Hugging Face. This allows us to build a zero-cost recommendation system while still leveraging the power of AI.",[104,46167,46169],{"id":46168},"understanding-embedding-dimensions-and-sizes","Understanding Embedding Dimensions and Sizes",[27,46171,46172],{},"In the world of embedding models, \"dimensions\" or \"sizes\" refer to the number of numerical values (or \"coordinates\") used to represent text in a numerical space.",[27,46174,46175],{},"Remember our earlier example with two-dimensional coordinates (x, y)? Embedding models work the same way but use hundreds or thousands of dimensions instead of just 2 or 3.",[27,46177,46178],{},"More dimensions allow the model to capture finer nuances and relationships between words and concepts - like having a more detailed meaning map. Fewer dimensions create a simpler representation, which produces a smaller numerical representation (the \"embedding\"). Higher dimensions offer better accuracy but require more storage and computing power.",[27,46180,46181,46182,114,46184,46186,46187,114,46190,46193,46194,114,46197,46199,46200,46203],{},"For example, OpenAI and VoyageAI offer various types of embedding models. Their larger models like ",[22,46183,46119],{},[22,46185,46134],{}," use ",[42,46188,46189],{},"3072",[42,46191,46192],{},"2048"," dimensions respectively, while their lighter models like ",[22,46195,46196],{},"embedding-3-small",[22,46198,46137],{}," use as few as ",[42,46201,46202],{},"512"," dimensions.",[104,46205,46207],{"id":46206},"using-hugging-face-and-transformerjs","Using Hugging Face and Transformer.js",[27,46209,46210],{},"For my blog article, I decided to use a free recommendation engine by leveraging the power of Hugging Face and Transformer.js.",[123,46212,46214],{"id":46213},"what-is-transformerjs","What is Transformer.js?",[27,46216,46217,46222],{},[45,46218,46221],{"href":46219,"target":2716,"rel":46220},"https:\u002F\u002Fhuggingface.co\u002Fdocs\u002Ftransformers.js\u002Fen\u002Findex",[2718,2719],"Transformer.js"," is a JavaScript library that lets you run AI models directly in your web browser or Node.js application. It downloads models locally, allowing them to run entirely on your device.",[27,46224,46225],{},"It supports various models including text generation, translation, image recognition, and embedding generation - all without relying on external APIs. This means zero usage fees, making it ideal for my personal blog project.",[123,46227,46229],{"id":46228},"choosing-the-right-model","Choosing the Right Model",[27,46231,46232,46233,1017],{},"Since the models in Transformer.js run locally, we need to be mindful not to choose a model that's too large in size and requires significant computing power to generate embeddings. For that purpose, I chose a model called ",[22,46234,28722],{},[27,46236,46237,46238,46241],{},"This model converts sentences and paragraphs into ",[42,46239,46240],{},"384","-dimensional dense vector embeddings, providing an excellent balance of performance and efficiency.",[27,46243,46244,46245,46252],{},"For compatibility with Transformers.js, I use the ",[45,46246,46249],{"href":46247,"target":2716,"rel":46248},"https:\u002F\u002Fhuggingface.co\u002FXenova\u002Fall-MiniLM-L6-v2",[2718,2719],[22,46250,46251],{},"Xenova\u002Fall-MiniLM-L6-v2"," model from Hugging Face.",[123,46254,46256],{"id":46255},"implementing-the-embedding-extraction","Implementing the Embedding Extraction",[27,46258,46259,46260,46263],{},"First, let's install the ",[22,46261,46262],{},"@huggingface\u002Ftransformers"," package:",[128,46265,46267],{"className":8665,"code":46266,"language":8667,"meta":133,"style":133},"npm install @huggingface\u002Ftransformers\n",[22,46268,46269],{"__ignoreMap":133},[137,46270,46271,46273,46275],{"class":139,"line":140},[137,46272,9536],{"class":147},[137,46274,10268],{"class":284},[137,46276,46277],{"class":284}," @huggingface\u002Ftransformers\n",[27,46279,46280],{},"Here's the simple code to compute embeddings from text:",[128,46282,46284],{"className":13299,"code":46283,"language":13301,"meta":133,"style":133},"import { pipeline } from \"@huggingface\u002Ftransformers\";\n\nconst text = \"The Importance of Machine Learning in Modern Web Development\";\n\nconst extractor = await pipeline(\"feature-extraction\", \"Xenova\u002Fall-MiniLM-L6-v2\");\nconst output = await extractor(text, { pooling: \"mean\", normalize: true });\nconst vector = output.tolist();\nconsole.log(vector[0]);\n",[22,46285,46286,46300,46304,46317,46321,46347,46373,46390],{"__ignoreMap":133},[137,46287,46288,46290,46293,46295,46298],{"class":139,"line":140},[137,46289,10287],{"class":143},[137,46291,46292],{"class":157}," { pipeline } ",[137,46294,10954],{"class":143},[137,46296,46297],{"class":284}," \"@huggingface\u002Ftransformers\"",[137,46299,3276],{"class":157},[137,46301,46302],{"class":139,"line":173},[137,46303,516],{"emptyLinePlaceholder":515},[137,46305,46306,46308,46310,46312,46315],{"class":139,"line":188},[137,46307,3077],{"class":143},[137,46309,9151],{"class":364},[137,46311,151],{"class":143},[137,46313,46314],{"class":284}," \"The Importance of Machine Learning in Modern Web Development\"",[137,46316,3276],{"class":157},[137,46318,46319],{"class":139,"line":269},[137,46320,516],{"emptyLinePlaceholder":515},[137,46322,46323,46325,46328,46330,46332,46335,46337,46340,46342,46345],{"class":139,"line":278},[137,46324,3077],{"class":143},[137,46326,46327],{"class":364}," extractor",[137,46329,151],{"class":143},[137,46331,15069],{"class":143},[137,46333,46334],{"class":147}," pipeline",[137,46336,356],{"class":157},[137,46338,46339],{"class":284},"\"feature-extraction\"",[137,46341,164],{"class":157},[137,46343,46344],{"class":284},"\"Xenova\u002Fall-MiniLM-L6-v2\"",[137,46346,1502],{"class":157},[137,46348,46349,46351,46354,46356,46358,46360,46363,46366,46369,46371],{"class":139,"line":291},[137,46350,3077],{"class":143},[137,46352,46353],{"class":364}," output",[137,46355,151],{"class":143},[137,46357,15069],{"class":143},[137,46359,46327],{"class":147},[137,46361,46362],{"class":157},"(text, { pooling: ",[137,46364,46365],{"class":284},"\"mean\"",[137,46367,46368],{"class":157},", normalize: ",[137,46370,3097],{"class":364},[137,46372,4168],{"class":157},[137,46374,46375,46377,46380,46382,46385,46388],{"class":139,"line":297},[137,46376,3077],{"class":143},[137,46378,46379],{"class":364}," vector",[137,46381,151],{"class":143},[137,46383,46384],{"class":157}," output.",[137,46386,46387],{"class":147},"tolist",[137,46389,924],{"class":157},[137,46391,46392,46394,46396,46399,46401],{"class":139,"line":302},[137,46393,1436],{"class":157},[137,46395,353],{"class":147},[137,46397,46398],{"class":157},"(vector[",[137,46400,6044],{"class":364},[137,46402,43556],{"class":157},[27,46404,46405],{},"The console log from the above code will output the following 384-dimensional vector:",[128,46407,46409],{"className":13299,"code":46408,"language":13301,"meta":133,"style":133},"[\n   -0.020516404882073402,   -0.00818917341530323,    0.02859281376004219,\n    0.039169277995824814,    0.07049212604761124,   -0.02238479070365429,\n    -0.03139492869377136,   -0.06332399696111679,   -0.04767751693725586,\n    -0.02019294537603855,   -0.05919709429144859,    0.04298273101449013,\n    0.016094526275992393,    -0.0438438318669796,  -0.024306558072566986,\n     0.01921924203634262,  -0.028016094118356705,   -0.03439173847436905,\n    -0.04121260717511177,   -0.10778705775737762,  -0.021788278594613075,\n    -0.03216289356350899,    0.01833938993513584,  -0.017994459718465805,\n  0.00037811059155501425,    0.04635584354400635,   0.019866760820150375,\n     0.05040336400270462,    0.06332680583000183,   -0.03538951650261879,\n   -0.005023809149861336,    -0.0143986064940691, -0.0052444711327552795,\n    0.023529207333922386,   -0.07226388156414032,    0.07632710039615631,\n     0.01021984126418829,   -0.00824225414544344,    0.03339964896440506,\n    -0.01060909777879715,   -0.09655245393514633,  -0.055546682327985764,\n   -0.012066159397363663, -0.0076921964064240456,    0.04580942541360855,\n     0.02390001341700554,   -0.08683866262435913,   -0.08001071214675903,\n     -0.0641876757144928,   0.017844615504145622,   -0.08620015531778336,\n    -0.08266746997833252,   0.022476131096482277,  -0.036314111202955246,\n    -0.06440844386816025,     0.0329846553504467,    0.03954195976257324,\n    -0.02818935364484787,   0.016931047663092613, -0.0027509541250765324,\n   -0.012003917247056961,   -0.08396296203136444,   0.020469816401600838,\n    0.007711503189057112,   0.039566002786159515,  -0.023063624277710915,\n   -0.001518305274657905,    0.03386910259723663,  -0.005670216400176287,\n     0.01568906381726265,    -0.0874529480934143,     0.0704861655831337,\n    -0.08309027552604675,    0.06774929165840149,  -0.013879545032978058,\n    -0.03932321444153786,  -0.026192566379904747,  0.0037247256841510534,\n    0.025735696777701378,   0.042777273803949356,  -0.006177722010761499,\n    0.016913456842303276,  -0.019507644698023796,    0.07432487607002258,\n    0.043784961104393005,    0.03311076760292053,   0.005122023168951273,\n   -0.019937295466661453,  -0.058375801891088486,   -0.07924377918243408,\n   -0.015307264402508736,  -0.026857662945985794,    0.04473648965358734,\n   -0.003576496383175254,   -0.03769933804869652,   0.016773588955402374,\n    0.008591416291892529,   -0.07192342728376389,   -0.03686879947781563,\n     0.09621886163949966,\n  ... 284 more items\n]\n",[22,46410,46411,46415,46439,46458,46482,46503,46525,46546,46569,46590,46607,46626,46649,46668,46687,46710,46731,46752,46774,46795,46815,46836,46857,46876,46897,46916,46937,46958,46977,46996,47013,47036,47057,47078,47099,47106,47117],{"__ignoreMap":133},[137,46412,46413],{"class":139,"line":140},[137,46414,28046],{"class":157},[137,46416,46417,46420,46423,46426,46428,46431,46434,46437],{"class":139,"line":173},[137,46418,46419],{"class":143},"   -",[137,46421,46422],{"class":364},"0.020516404882073402",[137,46424,46425],{"class":157},",   ",[137,46427,8215],{"class":143},[137,46429,46430],{"class":364},"0.00818917341530323",[137,46432,46433],{"class":157},",    ",[137,46435,46436],{"class":364},"0.02859281376004219",[137,46438,1961],{"class":157},[137,46440,46441,46444,46446,46449,46451,46453,46456],{"class":139,"line":188},[137,46442,46443],{"class":364},"    0.039169277995824814",[137,46445,46433],{"class":157},[137,46447,46448],{"class":364},"0.07049212604761124",[137,46450,46425],{"class":157},[137,46452,8215],{"class":143},[137,46454,46455],{"class":364},"0.02238479070365429",[137,46457,1961],{"class":157},[137,46459,46460,46463,46466,46468,46470,46473,46475,46477,46480],{"class":139,"line":269},[137,46461,46462],{"class":143},"    -",[137,46464,46465],{"class":364},"0.03139492869377136",[137,46467,46425],{"class":157},[137,46469,8215],{"class":143},[137,46471,46472],{"class":364},"0.06332399696111679",[137,46474,46425],{"class":157},[137,46476,8215],{"class":143},[137,46478,46479],{"class":364},"0.04767751693725586",[137,46481,1961],{"class":157},[137,46483,46484,46486,46489,46491,46493,46496,46498,46501],{"class":139,"line":278},[137,46485,46462],{"class":143},[137,46487,46488],{"class":364},"0.02019294537603855",[137,46490,46425],{"class":157},[137,46492,8215],{"class":143},[137,46494,46495],{"class":364},"0.05919709429144859",[137,46497,46433],{"class":157},[137,46499,46500],{"class":364},"0.04298273101449013",[137,46502,1961],{"class":157},[137,46504,46505,46508,46510,46512,46515,46518,46520,46523],{"class":139,"line":291},[137,46506,46507],{"class":364},"    0.016094526275992393",[137,46509,46433],{"class":157},[137,46511,8215],{"class":143},[137,46513,46514],{"class":364},"0.0438438318669796",[137,46516,46517],{"class":157},",  ",[137,46519,8215],{"class":143},[137,46521,46522],{"class":364},"0.024306558072566986",[137,46524,1961],{"class":157},[137,46526,46527,46530,46532,46534,46537,46539,46541,46544],{"class":139,"line":297},[137,46528,46529],{"class":364},"     0.01921924203634262",[137,46531,46517],{"class":157},[137,46533,8215],{"class":143},[137,46535,46536],{"class":364},"0.028016094118356705",[137,46538,46425],{"class":157},[137,46540,8215],{"class":143},[137,46542,46543],{"class":364},"0.03439173847436905",[137,46545,1961],{"class":157},[137,46547,46548,46550,46553,46555,46557,46560,46562,46564,46567],{"class":139,"line":302},[137,46549,46462],{"class":143},[137,46551,46552],{"class":364},"0.04121260717511177",[137,46554,46425],{"class":157},[137,46556,8215],{"class":143},[137,46558,46559],{"class":364},"0.10778705775737762",[137,46561,46517],{"class":157},[137,46563,8215],{"class":143},[137,46565,46566],{"class":364},"0.021788278594613075",[137,46568,1961],{"class":157},[137,46570,46571,46573,46576,46578,46581,46583,46585,46588],{"class":139,"line":662},[137,46572,46462],{"class":143},[137,46574,46575],{"class":364},"0.03216289356350899",[137,46577,46433],{"class":157},[137,46579,46580],{"class":364},"0.01833938993513584",[137,46582,46517],{"class":157},[137,46584,8215],{"class":143},[137,46586,46587],{"class":364},"0.017994459718465805",[137,46589,1961],{"class":157},[137,46591,46592,46595,46597,46600,46602,46605],{"class":139,"line":667},[137,46593,46594],{"class":364},"  0.00037811059155501425",[137,46596,46433],{"class":157},[137,46598,46599],{"class":364},"0.04635584354400635",[137,46601,46425],{"class":157},[137,46603,46604],{"class":364},"0.019866760820150375",[137,46606,1961],{"class":157},[137,46608,46609,46612,46614,46617,46619,46621,46624],{"class":139,"line":786},[137,46610,46611],{"class":364},"     0.05040336400270462",[137,46613,46433],{"class":157},[137,46615,46616],{"class":364},"0.06332680583000183",[137,46618,46425],{"class":157},[137,46620,8215],{"class":143},[137,46622,46623],{"class":364},"0.03538951650261879",[137,46625,1961],{"class":157},[137,46627,46628,46630,46633,46635,46637,46640,46642,46644,46647],{"class":139,"line":798},[137,46629,46419],{"class":143},[137,46631,46632],{"class":364},"0.005023809149861336",[137,46634,46433],{"class":157},[137,46636,8215],{"class":143},[137,46638,46639],{"class":364},"0.0143986064940691",[137,46641,164],{"class":157},[137,46643,8215],{"class":143},[137,46645,46646],{"class":364},"0.0052444711327552795",[137,46648,1961],{"class":157},[137,46650,46651,46654,46656,46658,46661,46663,46666],{"class":139,"line":803},[137,46652,46653],{"class":364},"    0.023529207333922386",[137,46655,46425],{"class":157},[137,46657,8215],{"class":143},[137,46659,46660],{"class":364},"0.07226388156414032",[137,46662,46433],{"class":157},[137,46664,46665],{"class":364},"0.07632710039615631",[137,46667,1961],{"class":157},[137,46669,46670,46673,46675,46677,46680,46682,46685],{"class":139,"line":931},[137,46671,46672],{"class":364},"     0.01021984126418829",[137,46674,46425],{"class":157},[137,46676,8215],{"class":143},[137,46678,46679],{"class":364},"0.00824225414544344",[137,46681,46433],{"class":157},[137,46683,46684],{"class":364},"0.03339964896440506",[137,46686,1961],{"class":157},[137,46688,46689,46691,46694,46696,46698,46701,46703,46705,46708],{"class":139,"line":1568},[137,46690,46462],{"class":143},[137,46692,46693],{"class":364},"0.01060909777879715",[137,46695,46425],{"class":157},[137,46697,8215],{"class":143},[137,46699,46700],{"class":364},"0.09655245393514633",[137,46702,46517],{"class":157},[137,46704,8215],{"class":143},[137,46706,46707],{"class":364},"0.055546682327985764",[137,46709,1961],{"class":157},[137,46711,46712,46714,46717,46719,46721,46724,46726,46729],{"class":139,"line":1573},[137,46713,46419],{"class":143},[137,46715,46716],{"class":364},"0.012066159397363663",[137,46718,164],{"class":157},[137,46720,8215],{"class":143},[137,46722,46723],{"class":364},"0.0076921964064240456",[137,46725,46433],{"class":157},[137,46727,46728],{"class":364},"0.04580942541360855",[137,46730,1961],{"class":157},[137,46732,46733,46736,46738,46740,46743,46745,46747,46750],{"class":139,"line":1578},[137,46734,46735],{"class":364},"     0.02390001341700554",[137,46737,46425],{"class":157},[137,46739,8215],{"class":143},[137,46741,46742],{"class":364},"0.08683866262435913",[137,46744,46425],{"class":157},[137,46746,8215],{"class":143},[137,46748,46749],{"class":364},"0.08001071214675903",[137,46751,1961],{"class":157},[137,46753,46754,46757,46760,46762,46765,46767,46769,46772],{"class":139,"line":1588},[137,46755,46756],{"class":143},"     -",[137,46758,46759],{"class":364},"0.0641876757144928",[137,46761,46425],{"class":157},[137,46763,46764],{"class":364},"0.017844615504145622",[137,46766,46425],{"class":157},[137,46768,8215],{"class":143},[137,46770,46771],{"class":364},"0.08620015531778336",[137,46773,1961],{"class":157},[137,46775,46776,46778,46781,46783,46786,46788,46790,46793],{"class":139,"line":1601},[137,46777,46462],{"class":143},[137,46779,46780],{"class":364},"0.08266746997833252",[137,46782,46425],{"class":157},[137,46784,46785],{"class":364},"0.022476131096482277",[137,46787,46517],{"class":157},[137,46789,8215],{"class":143},[137,46791,46792],{"class":364},"0.036314111202955246",[137,46794,1961],{"class":157},[137,46796,46797,46799,46802,46805,46808,46810,46813],{"class":139,"line":3802},[137,46798,46462],{"class":143},[137,46800,46801],{"class":364},"0.06440844386816025",[137,46803,46804],{"class":157},",     ",[137,46806,46807],{"class":364},"0.0329846553504467",[137,46809,46433],{"class":157},[137,46811,46812],{"class":364},"0.03954195976257324",[137,46814,1961],{"class":157},[137,46816,46817,46819,46822,46824,46827,46829,46831,46834],{"class":139,"line":3808},[137,46818,46462],{"class":143},[137,46820,46821],{"class":364},"0.02818935364484787",[137,46823,46425],{"class":157},[137,46825,46826],{"class":364},"0.016931047663092613",[137,46828,164],{"class":157},[137,46830,8215],{"class":143},[137,46832,46833],{"class":364},"0.0027509541250765324",[137,46835,1961],{"class":157},[137,46837,46838,46840,46843,46845,46847,46850,46852,46855],{"class":139,"line":3822},[137,46839,46419],{"class":143},[137,46841,46842],{"class":364},"0.012003917247056961",[137,46844,46425],{"class":157},[137,46846,8215],{"class":143},[137,46848,46849],{"class":364},"0.08396296203136444",[137,46851,46425],{"class":157},[137,46853,46854],{"class":364},"0.020469816401600838",[137,46856,1961],{"class":157},[137,46858,46859,46862,46864,46867,46869,46871,46874],{"class":139,"line":3827},[137,46860,46861],{"class":364},"    0.007711503189057112",[137,46863,46425],{"class":157},[137,46865,46866],{"class":364},"0.039566002786159515",[137,46868,46517],{"class":157},[137,46870,8215],{"class":143},[137,46872,46873],{"class":364},"0.023063624277710915",[137,46875,1961],{"class":157},[137,46877,46878,46880,46883,46885,46888,46890,46892,46895],{"class":139,"line":3832},[137,46879,46419],{"class":143},[137,46881,46882],{"class":364},"0.001518305274657905",[137,46884,46433],{"class":157},[137,46886,46887],{"class":364},"0.03386910259723663",[137,46889,46517],{"class":157},[137,46891,8215],{"class":143},[137,46893,46894],{"class":364},"0.005670216400176287",[137,46896,1961],{"class":157},[137,46898,46899,46902,46904,46906,46909,46911,46914],{"class":139,"line":3840},[137,46900,46901],{"class":364},"     0.01568906381726265",[137,46903,46433],{"class":157},[137,46905,8215],{"class":143},[137,46907,46908],{"class":364},"0.0874529480934143",[137,46910,46804],{"class":157},[137,46912,46913],{"class":364},"0.0704861655831337",[137,46915,1961],{"class":157},[137,46917,46918,46920,46923,46925,46928,46930,46932,46935],{"class":139,"line":3846},[137,46919,46462],{"class":143},[137,46921,46922],{"class":364},"0.08309027552604675",[137,46924,46433],{"class":157},[137,46926,46927],{"class":364},"0.06774929165840149",[137,46929,46517],{"class":157},[137,46931,8215],{"class":143},[137,46933,46934],{"class":364},"0.013879545032978058",[137,46936,1961],{"class":157},[137,46938,46939,46941,46944,46946,46948,46951,46953,46956],{"class":139,"line":3861},[137,46940,46462],{"class":143},[137,46942,46943],{"class":364},"0.03932321444153786",[137,46945,46517],{"class":157},[137,46947,8215],{"class":143},[137,46949,46950],{"class":364},"0.026192566379904747",[137,46952,46517],{"class":157},[137,46954,46955],{"class":364},"0.0037247256841510534",[137,46957,1961],{"class":157},[137,46959,46960,46963,46965,46968,46970,46972,46975],{"class":139,"line":3883},[137,46961,46962],{"class":364},"    0.025735696777701378",[137,46964,46425],{"class":157},[137,46966,46967],{"class":364},"0.042777273803949356",[137,46969,46517],{"class":157},[137,46971,8215],{"class":143},[137,46973,46974],{"class":364},"0.006177722010761499",[137,46976,1961],{"class":157},[137,46978,46979,46982,46984,46986,46989,46991,46994],{"class":139,"line":3896},[137,46980,46981],{"class":364},"    0.016913456842303276",[137,46983,46517],{"class":157},[137,46985,8215],{"class":143},[137,46987,46988],{"class":364},"0.019507644698023796",[137,46990,46433],{"class":157},[137,46992,46993],{"class":364},"0.07432487607002258",[137,46995,1961],{"class":157},[137,46997,46998,47001,47003,47006,47008,47011],{"class":139,"line":3901},[137,46999,47000],{"class":364},"    0.043784961104393005",[137,47002,46433],{"class":157},[137,47004,47005],{"class":364},"0.03311076760292053",[137,47007,46425],{"class":157},[137,47009,47010],{"class":364},"0.005122023168951273",[137,47012,1961],{"class":157},[137,47014,47015,47017,47020,47022,47024,47027,47029,47031,47034],{"class":139,"line":3906},[137,47016,46419],{"class":143},[137,47018,47019],{"class":364},"0.019937295466661453",[137,47021,46517],{"class":157},[137,47023,8215],{"class":143},[137,47025,47026],{"class":364},"0.058375801891088486",[137,47028,46425],{"class":157},[137,47030,8215],{"class":143},[137,47032,47033],{"class":364},"0.07924377918243408",[137,47035,1961],{"class":157},[137,47037,47038,47040,47043,47045,47047,47050,47052,47055],{"class":139,"line":3911},[137,47039,46419],{"class":143},[137,47041,47042],{"class":364},"0.015307264402508736",[137,47044,46517],{"class":157},[137,47046,8215],{"class":143},[137,47048,47049],{"class":364},"0.026857662945985794",[137,47051,46433],{"class":157},[137,47053,47054],{"class":364},"0.04473648965358734",[137,47056,1961],{"class":157},[137,47058,47059,47061,47064,47066,47068,47071,47073,47076],{"class":139,"line":4666},[137,47060,46419],{"class":143},[137,47062,47063],{"class":364},"0.003576496383175254",[137,47065,46425],{"class":157},[137,47067,8215],{"class":143},[137,47069,47070],{"class":364},"0.03769933804869652",[137,47072,46425],{"class":157},[137,47074,47075],{"class":364},"0.016773588955402374",[137,47077,1961],{"class":157},[137,47079,47080,47083,47085,47087,47090,47092,47094,47097],{"class":139,"line":4672},[137,47081,47082],{"class":364},"    0.008591416291892529",[137,47084,46425],{"class":157},[137,47086,8215],{"class":143},[137,47088,47089],{"class":364},"0.07192342728376389",[137,47091,46425],{"class":157},[137,47093,8215],{"class":143},[137,47095,47096],{"class":364},"0.03686879947781563",[137,47098,1961],{"class":157},[137,47100,47101,47104],{"class":139,"line":4680},[137,47102,47103],{"class":364},"     0.09621886163949966",[137,47105,1961],{"class":157},[137,47107,47108,47111,47114],{"class":139,"line":4711},[137,47109,47110],{"class":143},"  ...",[137,47112,47113],{"class":364}," 284",[137,47115,47116],{"class":157}," more items\n",[137,47118,47119],{"class":139,"line":4716},[137,47120,33307],{"class":157},[27,47122,47123],{},"At first glance, these numbers might look meaningless, but trust me - we'll see later how these numbers help us compare vectors of different texts to determine their similarity. But before that, let's see what each part of the code above does:",[1003,47125,47126,47139,47152,47165],{},[1006,47127,47128,47131,47132,47135,47136,47138],{},[42,47129,47130],{},"Load a Feature Extraction Model"," – The ",[22,47133,47134],{},"pipeline"," function loads the ",[22,47137,46251],{}," model. It downloads once and caches locally, making subsequent runs faster.",[1006,47140,47141,47144,47145,47148,47149,47151],{},[42,47142,47143],{},"Process the Text"," – The extractor converts the input text (",[22,47146,47147],{},"\"The Importance of Machine Learning in Modern Web Development\"",") into a ",[42,47150,46240],{},"-dimensional vector.",[1006,47153,47154,47131,47157,47160,47161,47164],{},[42,47155,47156],{},"Pooling & Normalisation",[22,47158,47159],{},"pooling: \"mean\""," option averages the token embeddings, while ",[22,47162,47163],{},"normalize: true"," ensures consistent scaling.",[1006,47166,47167,47170],{},[42,47168,47169],{},"Convert to Array & Output"," – The resulting vector is converted to an array and logged to the console.",[104,47172,47174],{"id":47173},"calculate-cosine-similarity-between-two-embeddings","Calculate cosine similarity between two embeddings",[27,47176,47177],{},"Now that we have extracted embeddings from a single text, let's compare the similarity between two different texts using cosine similarity. Cosine similarity is a common metric for measuring the similarity between two vectors, with a value between -1 (completely opposite) and 1 (identical).",[27,47179,47180,47181,47184],{},"For calculating the cosine similarity, we'll use a helper library for Node.js from TensorFlow called ",[22,47182,47183],{},"@tensorflow\u002Ftfjs-node",". First, we need to ensure we have this package installed:",[128,47186,47188],{"className":8665,"code":47187,"language":8667,"meta":133,"style":133},"npm install @tensorflow\u002Ftfjs-node\n",[22,47189,47190],{"__ignoreMap":133},[137,47191,47192,47194,47196],{"class":139,"line":140},[137,47193,9536],{"class":147},[137,47195,10268],{"class":284},[137,47197,47198],{"class":284}," @tensorflow\u002Ftfjs-node\n",[123,47200,47202],{"id":47201},"compute-embeddings-for-two-texts","Compute Embeddings for Two Texts",[128,47204,47206],{"className":13299,"code":47205,"language":13301,"meta":133,"style":133},"import { pipeline } from \"@huggingface\u002Ftransformers\";\nimport * as tf from \"@tensorflow\u002Ftfjs-node\";\n\n\u002F\u002F Define two different texts\nconst text1 = \"The Importance of Machine Learning in Modern Web Development\";\nconst text2 = \"Machine learning is transforming the future of web applications.\";\n\n\u002F\u002F Load the feature extraction model\nconst extractor = await pipeline(\"feature-extraction\", \"Xenova\u002Fall-MiniLM-L6-v2\");\n\n\u002F\u002F Extract embeddings for both texts\nconst output1 = await extractor(text1, { pooling: \"mean\", normalize: true });\nconst output2 = await extractor(text2, { pooling: \"mean\", normalize: true });\n\nconst vector1 = output1.tolist();\nconst vector2 = output2.tolist();\n",[22,47207,47208,47220,47238,47242,47247,47260,47274,47278,47283,47305,47309,47314,47338,47362,47366,47382],{"__ignoreMap":133},[137,47209,47210,47212,47214,47216,47218],{"class":139,"line":140},[137,47211,10287],{"class":143},[137,47213,46292],{"class":157},[137,47215,10954],{"class":143},[137,47217,46297],{"class":284},[137,47219,3276],{"class":157},[137,47221,47222,47224,47226,47228,47231,47233,47236],{"class":139,"line":173},[137,47223,10287],{"class":143},[137,47225,13878],{"class":364},[137,47227,13881],{"class":143},[137,47229,47230],{"class":157}," tf ",[137,47232,10954],{"class":143},[137,47234,47235],{"class":284}," \"@tensorflow\u002Ftfjs-node\"",[137,47237,3276],{"class":157},[137,47239,47240],{"class":139,"line":188},[137,47241,516],{"emptyLinePlaceholder":515},[137,47243,47244],{"class":139,"line":269},[137,47245,47246],{"class":308},"\u002F\u002F Define two different texts\n",[137,47248,47249,47251,47254,47256,47258],{"class":139,"line":278},[137,47250,3077],{"class":143},[137,47252,47253],{"class":364}," text1",[137,47255,151],{"class":143},[137,47257,46314],{"class":284},[137,47259,3276],{"class":157},[137,47261,47262,47264,47267,47269,47272],{"class":139,"line":291},[137,47263,3077],{"class":143},[137,47265,47266],{"class":364}," text2",[137,47268,151],{"class":143},[137,47270,47271],{"class":284}," \"Machine learning is transforming the future of web applications.\"",[137,47273,3276],{"class":157},[137,47275,47276],{"class":139,"line":297},[137,47277,516],{"emptyLinePlaceholder":515},[137,47279,47280],{"class":139,"line":302},[137,47281,47282],{"class":308},"\u002F\u002F Load the feature extraction model\n",[137,47284,47285,47287,47289,47291,47293,47295,47297,47299,47301,47303],{"class":139,"line":662},[137,47286,3077],{"class":143},[137,47288,46327],{"class":364},[137,47290,151],{"class":143},[137,47292,15069],{"class":143},[137,47294,46334],{"class":147},[137,47296,356],{"class":157},[137,47298,46339],{"class":284},[137,47300,164],{"class":157},[137,47302,46344],{"class":284},[137,47304,1502],{"class":157},[137,47306,47307],{"class":139,"line":667},[137,47308,516],{"emptyLinePlaceholder":515},[137,47310,47311],{"class":139,"line":786},[137,47312,47313],{"class":308},"\u002F\u002F Extract embeddings for both texts\n",[137,47315,47316,47318,47321,47323,47325,47327,47330,47332,47334,47336],{"class":139,"line":798},[137,47317,3077],{"class":143},[137,47319,47320],{"class":364}," output1",[137,47322,151],{"class":143},[137,47324,15069],{"class":143},[137,47326,46327],{"class":147},[137,47328,47329],{"class":157},"(text1, { pooling: ",[137,47331,46365],{"class":284},[137,47333,46368],{"class":157},[137,47335,3097],{"class":364},[137,47337,4168],{"class":157},[137,47339,47340,47342,47345,47347,47349,47351,47354,47356,47358,47360],{"class":139,"line":803},[137,47341,3077],{"class":143},[137,47343,47344],{"class":364}," output2",[137,47346,151],{"class":143},[137,47348,15069],{"class":143},[137,47350,46327],{"class":147},[137,47352,47353],{"class":157},"(text2, { pooling: ",[137,47355,46365],{"class":284},[137,47357,46368],{"class":157},[137,47359,3097],{"class":364},[137,47361,4168],{"class":157},[137,47363,47364],{"class":139,"line":931},[137,47365,516],{"emptyLinePlaceholder":515},[137,47367,47368,47370,47373,47375,47378,47380],{"class":139,"line":1568},[137,47369,3077],{"class":143},[137,47371,47372],{"class":364}," vector1",[137,47374,151],{"class":143},[137,47376,47377],{"class":157}," output1.",[137,47379,46387],{"class":147},[137,47381,924],{"class":157},[137,47383,47384,47386,47389,47391,47394,47396],{"class":139,"line":1573},[137,47385,3077],{"class":143},[137,47387,47388],{"class":364}," vector2",[137,47390,151],{"class":143},[137,47392,47393],{"class":157}," output2.",[137,47395,46387],{"class":147},[137,47397,924],{"class":157},[123,47399,47401],{"id":47400},"compute-cosine-similarity","Compute Cosine Similarity",[128,47403,47405],{"className":13299,"code":47404,"language":13301,"meta":133,"style":133},"\u002F\u002F Function to calculate cosine similarity\nfunction cosineSimilarity(vec1, vec2) {\n    const dotProduct = tf.sum(tf.mul(vec1, vec2));\n    const magnitude1 = tf.norm(vec1);\n    const magnitude2 = tf.norm(vec2);\n    return dotProduct.div(magnitude1.mul(magnitude2)).dataSync()[0];\n}\n\n\u002F\u002F Calculate similarity\nconst similarityScore = cosineSimilarity(vector1, vector2);\nconsole.log(\"Cosine Similarity Score:\", similarityScore);\n",[22,47406,47407,47412,47431,47455,47472,47488,47514,47518,47522,47527,47541],{"__ignoreMap":133},[137,47408,47409],{"class":139,"line":140},[137,47410,47411],{"class":308},"\u002F\u002F Function to calculate cosine similarity\n",[137,47413,47414,47416,47419,47421,47424,47426,47429],{"class":139,"line":173},[137,47415,483],{"class":143},[137,47417,47418],{"class":147}," cosineSimilarity",[137,47420,356],{"class":157},[137,47422,47423],{"class":161},"vec1",[137,47425,164],{"class":157},[137,47427,47428],{"class":161},"vec2",[137,47430,170],{"class":157},[137,47432,47433,47435,47438,47440,47443,47446,47449,47452],{"class":139,"line":188},[137,47434,4177],{"class":143},[137,47436,47437],{"class":364}," dotProduct",[137,47439,151],{"class":143},[137,47441,47442],{"class":157}," tf.",[137,47444,47445],{"class":147},"sum",[137,47447,47448],{"class":157},"(tf.",[137,47450,47451],{"class":147},"mul",[137,47453,47454],{"class":157},"(vec1, vec2));\n",[137,47456,47457,47459,47462,47464,47466,47469],{"class":139,"line":269},[137,47458,4177],{"class":143},[137,47460,47461],{"class":364}," magnitude1",[137,47463,151],{"class":143},[137,47465,47442],{"class":157},[137,47467,47468],{"class":147},"norm",[137,47470,47471],{"class":157},"(vec1);\n",[137,47473,47474,47476,47479,47481,47483,47485],{"class":139,"line":278},[137,47475,4177],{"class":143},[137,47477,47478],{"class":364}," magnitude2",[137,47480,151],{"class":143},[137,47482,47442],{"class":157},[137,47484,47468],{"class":147},[137,47486,47487],{"class":157},"(vec2);\n",[137,47489,47490,47492,47495,47497,47500,47502,47505,47508,47510,47512],{"class":139,"line":291},[137,47491,176],{"class":143},[137,47493,47494],{"class":157}," dotProduct.",[137,47496,8330],{"class":147},[137,47498,47499],{"class":157},"(magnitude1.",[137,47501,47451],{"class":147},[137,47503,47504],{"class":157},"(magnitude2)).",[137,47506,47507],{"class":147},"dataSync",[137,47509,14210],{"class":157},[137,47511,6044],{"class":364},[137,47513,5727],{"class":157},[137,47515,47516],{"class":139,"line":297},[137,47517,510],{"class":157},[137,47519,47520],{"class":139,"line":302},[137,47521,516],{"emptyLinePlaceholder":515},[137,47523,47524],{"class":139,"line":662},[137,47525,47526],{"class":308},"\u002F\u002F Calculate similarity\n",[137,47528,47529,47531,47534,47536,47538],{"class":139,"line":667},[137,47530,3077],{"class":143},[137,47532,47533],{"class":364}," similarityScore",[137,47535,151],{"class":143},[137,47537,47418],{"class":147},[137,47539,47540],{"class":157},"(vector1, vector2);\n",[137,47542,47543,47545,47547,47549,47552],{"class":139,"line":786},[137,47544,1436],{"class":157},[137,47546,353],{"class":147},[137,47548,356],{"class":157},[137,47550,47551],{"class":284},"\"Cosine Similarity Score:\"",[137,47553,47554],{"class":157},", similarityScore);\n",[123,47556,47558],{"id":47557},"understanding-the-results","Understanding the Results",[27,47560,47561],{},"The cosine similarity score indicates how semantically similar two texts are. A score closer to 1 means the texts are very similar, while a lower score indicates they are less related.",[27,47563,47564],{},"For example, from above the output will be like:",[128,47566,47568],{"className":8665,"code":47567,"language":8667,"meta":133,"style":133},"Cosine Similarity Score: 0.787977933883667\n",[22,47569,47570],{"__ignoreMap":133},[137,47571,47572,47575,47578,47581],{"class":139,"line":140},[137,47573,47574],{"class":147},"Cosine",[137,47576,47577],{"class":284}," Similarity",[137,47579,47580],{"class":284}," Score:",[137,47582,47583],{"class":364}," 0.787977933883667\n",[27,47585,47586,47587,3955,47590,1017],{},"This indicates that the two texts are quite similar in meaning. If we compared completely different topics, the score would be much lower, like ",[22,47588,47589],{},"0.2",[22,47591,47592],{},"0.3",[104,47594,47596],{"id":47595},"similar-article-suggestions-in-my-blog","Similar Article Suggestions in My Blog",[27,47598,47599],{},"Now that we understand how to generate embeddings and calculate cosine similarity, let me show you how I implemented this in my personal blog. Built with Nuxt.js and the Nuxt Content module, my blog stores articles in Markdown format, providing a structured way to access and process content.",[27,47601,47602],{},"The first step was extracting textual content from all blog articles' Markdown files. During static site generation, I used the embedding extraction code we discussed earlier to generate a numerical vector representation for each blog post.",[27,47604,47605],{},"With all articles converted to numerical vectors, the next step was determining article similarity. When someone reads a blog post, my system calculates the cosine similarity between that article's vector and all other articles' vectors. This reveals which articles are most semantically related.",[27,47607,47608],{},"I then select the three articles with the highest cosine similarity scores. These closest content matches appear at the bottom of the current article as reading suggestions.",[27,47610,47611,47612,47614,47615,164,47617,164,47619],{},"For example, when a user reads an article about ",[42,47613,16640],{},", they'll see suggestions for: ",[42,47616,12814],{},[42,47618,19586],{},[42,47620,2666],{},[27,47622,47623],{},[63,47624],{"alt":47625,"src":47626},"Image Three","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1742804587\u002Fblog\u002Fcreate-a-recommendation-engine-with-ai-for-free\u002Fimage-3_effv29",[27,47628,47629,47630,1017],{},"For those interested in seeing the implementation details, the complete source code of my blog is available on GitHub ",[45,47631,10647],{"href":47632,"target":2716,"rel":47633},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fpersonal-blog-2023",[2718,2719],[27,47635,47636,47637,1017],{},"The example above is available on this repository ",[45,47638,10647],{"href":47639,"target":2716,"rel":47640},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fcreate-a-recommendation-engine-with-ai-for-free",[2718,2719],[2617,47642,47643],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":133,"searchDepth":173,"depth":173,"links":47645},[47646,47647,47651,47652,47657,47662],{"id":46031,"depth":173,"text":46032},{"id":46102,"depth":173,"text":46103,"children":47648},[47649,47650],{"id":46109,"depth":188,"text":46110},{"id":46152,"depth":188,"text":46155},{"id":46168,"depth":173,"text":46169},{"id":46206,"depth":173,"text":46207,"children":47653},[47654,47655,47656],{"id":46213,"depth":188,"text":46214},{"id":46228,"depth":188,"text":46229},{"id":46255,"depth":188,"text":46256},{"id":47173,"depth":173,"text":47174,"children":47658},[47659,47660,47661],{"id":47201,"depth":188,"text":47202},{"id":47400,"depth":188,"text":47401},{"id":47557,"depth":188,"text":47558},{"id":47595,"depth":173,"text":47596},"Learn how to build a free AI-powered recommendation engine using open-source embedding models from Hugging Face and Transformer.js. This step-by-step guide will show you how to generate text embeddings, compare article similarities, and create personalised content suggestions - without relying on paid APIs.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1774661369\u002Fblog\u002Fcreate-a-recommendation-engine-with-ai-for-free\u002Fcreate_a_recommendation_engine_with_ai_for_free_slimzx",[27886,28597,9,46221,47666,47667,27992,47668,47669,47670,47671,47672,47673],"AI recommendation engine","Free AI models","Embedding models","Blog recommendations","AI article suggestions","Open-source AI","Text embeddings","JavaScript AI",{},"\u002F2025\u002F03\u002F24\u002Fcreate-a-recommendation-engine-with-ai-for-free","24th March 2025",{"title":45979,"description":47663},"2025\u002F03\u002F24\u002Fcreate-a-recommendation-engine-with-ai-for-free","SiPZV8iCOxvrZxauQ6sonEtd1dNuvnezVH3kD9d_jaY",{"id":47681,"title":47682,"articleTags":47683,"author":11,"blog":12,"body":47684,"description":52166,"extension":2649,"image":52167,"keywords":52168,"meta":52197,"navigation":515,"path":52198,"published":52199,"readTime":798,"seo":52200,"stem":52201,"type":2662,"__hash__":52202},"content\u002F2025\u002F04\u002F05\u002Fbuilding-an-ai-search-engine-with-nextjs-and-free-open-source-llms.md","Building an AI Search Engine with Next.js and Free Open-Source LLMs",[29177,2669,27886],{"type":14,"value":47685,"toc":52143},[47686,47689,47703,47705,47709,47714,47721,47724,47727,47746,47753,47756,47760,47782,47786,47800,47811,47815,47818,47821,47824,47838,47841,47845,47848,47853,47856,47863,47869,47873,47876,47881,47884,47889,47893,47896,47911,47918,47922,47928,47931,47951,47954,47993,48003,48007,48011,48014,48017,48021,48028,48032,48035,48049,48052,48075,48078,48377,48380,48384,48397,48401,48406,49137,49143,49149,49166,49173,49187,49194,49198,49207,49210,49222,49225,49228,49270,49273,49317,49320,49411,49414,49418,49424,49466,49469,49498,49501,49820,49823,49827,49830,49833,49837,49851,49854,49864,50336,50363,50367,50370,50377,50846,50863,50867,50878,50886,51009,51017,51343,51351,51389,51407,52126,52130,52137,52140],[17,47687,47682],{"id":47688},"building-an-ai-search-engine-with-nextjs-and-free-open-source-llms",[27,47690,47691],{},[30,47692,47693,36,47695,40,47697],{},[33,47694],{"value":35},[33,47696],{"value":39},[42,47698,47699],{},[45,47700,47701],{"href":47},[33,47702],{"value":50},[52,47704],{":tags":54},[56,47706],{":audio-src":47707,":transcript-src":47708},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F04\u002F05\u002Fbuilding-an-ai-search-engine-with-nextjs-and-free-open-source-llms\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F04\u002F05\u002Fbuilding-an-ai-search-engine-with-nextjs-and-free-open-source-llms\u002Fsummary.json",[27,47710,47711],{},[63,47712],{"alt":12847,"src":47713},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1743826108\u002Fblog\u002Fbuilding-an-ai-search-engine-with-nextjs-and-free-open-source-llms\u002FChatGPT_Image_Apr_5_2025_01_58_48_PM_famrgp",[27,47715,47716,47717,47720],{},"Following my article on ",[47718,47719,45979],"nuxt-link",{"to":47675},", I'd like to show you how to build an intelligent search engine.",[27,47722,47723],{},"We'll create an AI search engine using Next.js as our foundation and harness the power of free, open-source LLMs, without relying on expensive APIs or proprietary models.",[27,47725,47726],{},"Here are the key technologies we'll use:",[1003,47728,47729,47737],{},[1006,47730,47731,114,47733,47736],{},[42,47732,33549],{},[42,47734,47735],{},"vector databases"," for efficient search indexing",[1006,47738,47739,26741,47742,47745],{},[42,47740,47741],{},"Open-source models from Hugging Face",[45,47743,46221],{"href":46219,"target":2716,"rel":47744},[2718,2719]," for AI-powered search",[27,47747,47748,47749,47752],{},"By the end of this article, you'll learn how to ",[42,47750,47751],{},"combine Retrieval-Augmented Generation (RAG) with local free open-source models"," to create a powerful, privacy-friendly, and cost-effective search engine.",[27,47754,47755],{},"First, let's explore the benefits of AI-powered search and understand how RAG enhances our solution.",[104,47757,47759],{"id":47758},"why-build-an-ai-powered-search-engine","Why Build an AI-Powered Search Engine?",[27,47761,47762,47763,47766,47767,47770,47771,47774,47775,47778,47779,1017],{},"Traditional search engines rely on ",[42,47764,47765],{},"keyword matching",", often producing ",[42,47768,47769],{},"irrelevant or incomplete results",". In contrast, AI-powered search engines use ",[42,47772,47773],{},"LLMs and vector search"," to understand the ",[42,47776,47777],{},"meaning"," behind queries rather than simply matching words. This makes searches more ",[42,47780,47781],{},"intuitive, context-aware, and accurate",[123,47783,47785],{"id":47784},"why-ai-search-needs-more-than-just-llms","Why AI Search Needs More Than Just LLMs",[27,47787,47788,47789,47792,47793,47796,47797,1017],{},"While AI-powered search engines excel at information retrieval, they face a significant limitation—",[42,47790,47791],{},"most LLMs rely only on their pre-trained data",". This means they ",[42,47794,47795],{},"can't access real-time or external knowledge",", which is essential for ",[42,47798,47799],{},"accurate and up-to-date search results",[27,47801,47802,47803,47806,47807,47810],{},"Enter ",[42,47804,47805],{},"RAG",". Instead of depending solely on an LLM's internal knowledge, RAG ",[42,47808,47809],{},"retrieves relevant data"," from external sources before generating responses. Let's explore how this works.",[104,47812,47814],{"id":47813},"what-is-retrieval-augmented-generation-rag","What is Retrieval-Augmented Generation (RAG)?",[27,47816,47817],{},"Standard LLMs are constrained by their training data and lack real-time knowledge. RAG overcomes this limitation by retrieving relevant information from external sources to generate accurate responses.",[27,47819,47820],{},"This approach delivers up-to-date information, enables smaller models to perform better through external knowledge, and improves accuracy with fact-based responses.",[27,47822,47823],{},"RAG combines two essential components:",[2569,47825,47826,47832],{},[1006,47827,47828,47831],{},[42,47829,47830],{},"Retrieval",": Searching a database for relevant information based on a user query.",[1006,47833,47834,47837],{},[42,47835,47836],{},"Generation",": Using an LLM to generate a response based on the retrieved information.",[27,47839,47840],{},"This method shines when building AI systems that need accurate, context-aware answers. Rather than relying solely on the LLM's knowledge, RAG pulls up-to-date and domain-specific information from a database, ensuring responses are both relevant and accurate.",[104,47842,47844],{"id":47843},"the-rag-flow-process","The RAG Flow Process",[27,47846,47847],{},"Now that we understand RAG's capabilities, let's examine how everything fits together in our AI search engine.",[27,47849,47850],{},[63,47851],{"alt":47844,"src":47852},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1743826147\u002Fblog\u002Fbuilding-an-ai-search-engine-with-nextjs-and-free-open-source-llms\u002FChatGPT_Image_Apr_2_2025_04_38_23_PM_w5x7og",[27,47854,47855],{},"Here's the search process: When users submit questions, the system converts them into queries for the embedding model. This model transforms text into numerical vectors (embeddings) that capture semantic meaning. These embeddings search the vector database, finding similar content through cosine similarity calculations. The system then passes relevant data to an LLM, which generates contextually appropriate answers based on the retrieved information. Finally, it presents these answers to users, completing the search cycle.",[27,47857,47858,47859,47862],{},"Embeddings are crucial for AI search functionality. They transform text into numerical vectors that capture meaning. For instance, the sentence \"AI is amazing\" might become a vector like ",[22,47860,47861],{},"[0.1, 0.5, -0.3, ...]",". This numerical representation helps measure semantic similarity between texts.",[27,47864,47865,47866,47868],{},"For more about embeddings and search techniques, see my article ",[47718,47867,45979],{"to":47675},". Since that's beyond our current scope, I'll leave those details for your further reading.",[104,47870,47872],{"id":47871},"the-ai-search-engine-in-action","The AI Search Engine in Action",[27,47874,47875],{},"Before we start building, let me show you our target product. Here's a wireframe of the interface:",[27,47877,47878],{},[63,47879],{"alt":47872,"src":47880},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1743826157\u002Fblog\u002Fbuilding-an-ai-search-engine-with-nextjs-and-free-open-source-llms\u002FChatGPT_Image_Apr_3_2025_11_27_27_AM_pq7aim",[27,47882,47883],{},"The wireframe shows a simple input field for user questions. When users press enter or click search, the AI responds with a generated answer and links to relevant documents.",[3244,47885,47886],{},[27,47887,47888],{},"To demonstrate the AI search capabilities, I'll use articles from my personal blog. We'll import these into an SQLite database to enable RAG functionality—more details coming up.",[104,47890,47892],{"id":47891},"project-setup","Project Setup",[27,47894,47895],{},"Start by creating a Next.js app:",[128,47897,47899],{"className":8665,"code":47898,"language":8667,"meta":133,"style":133},"npx create-next-app@latest my-nextjs-app\n",[22,47900,47901],{"__ignoreMap":133},[137,47902,47903,47905,47908],{"class":139,"line":140},[137,47904,21167],{"class":147},[137,47906,47907],{"class":284}," create-next-app@latest",[137,47909,47910],{"class":284}," my-nextjs-app\n",[27,47912,47913,47914,1017],{},"For detailed setup instructions, check the docs ",[45,47915,10647],{"href":47916,"target":2716,"rel":47917},"https:\u002F\u002Fnextjs.org\u002Fdocs\u002Fapp\u002Fgetting-started\u002Finstallation",[2718,2719],[123,47919,47921],{"id":47920},"setting-up-local-open-source-models","Setting Up Local Open-Source Models",[27,47923,22292,47924,47927],{},[22,47925,47926],{},"local_models"," directory in your project root to store downloaded models.",[27,47929,47930],{},"Download these two essential models from Hugging Face:",[2569,47932,47933,47941],{},[1006,47934,47935,47940],{},[45,47936,47938],{"href":46247,"target":2716,"rel":47937},[2718,2719],[42,47939,28722],{},": A lightweight model for generating embeddings.",[1006,47942,47943,47950],{},[45,47944,47947],{"href":47945,"target":2716,"rel":47946},"https:\u002F\u002Fhuggingface.co\u002FXenova\u002FPhi-3-mini-4k-instruct",[2718,2719],[42,47948,47949],{},"Phi-3-mini-4k-instruct",": A compact but powerful model for text generation from Microsoft.",[27,47952,47953],{},"Use these commands to download the models:",[128,47955,47957],{"className":8665,"code":47956,"language":8667,"meta":133,"style":133},"# Download all-MiniLM-L6-v2\ngit clone https:\u002F\u002Fhuggingface.co\u002FXenova\u002Fall-MiniLM-L6-v2\n\n# Download Phi-3-mini-4k-instruct\ngit clone https:\u002F\u002Fhuggingface.co\u002FXenova\u002FPhi-3-mini-4k-instruct\n",[22,47958,47959,47964,47975,47979,47984],{"__ignoreMap":133},[137,47960,47961],{"class":139,"line":140},[137,47962,47963],{"class":308},"# Download all-MiniLM-L6-v2\n",[137,47965,47966,47969,47972],{"class":139,"line":173},[137,47967,47968],{"class":147},"git",[137,47970,47971],{"class":284}," clone",[137,47973,47974],{"class":284}," https:\u002F\u002Fhuggingface.co\u002FXenova\u002Fall-MiniLM-L6-v2\n",[137,47976,47977],{"class":139,"line":188},[137,47978,516],{"emptyLinePlaceholder":515},[137,47980,47981],{"class":139,"line":269},[137,47982,47983],{"class":308},"# Download Phi-3-mini-4k-instruct\n",[137,47985,47986,47988,47990],{"class":139,"line":278},[137,47987,47968],{"class":147},[137,47989,47971],{"class":284},[137,47991,47992],{"class":284}," https:\u002F\u002Fhuggingface.co\u002FXenova\u002FPhi-3-mini-4k-instruct\n",[27,47994,47995,47996,36830,48000],{},"While these models are lightweight compared to larger alternatives, they still occupy hundreds of megabytes. Download times depend on your internet speed. Since the repositories contain large files, first install Git Large File Storage (git-lfs) from ",[45,47997,47998],{"href":47998,"rel":47999},"https:\u002F\u002Fgit-lfs.com",[10924],[22,48001,48002],{},"git lfs install",[123,48004,48006],{"id":48005},"setting-up-sqlite-and-the-vector-database","Setting Up SQLite and the Vector Database",[2855,48008,48010],{"id":48009},"why-sqlite","Why SQLite?",[27,48012,48013],{},"SQLite is a lightweight, file-based database perfect for small to medium-sized projects. We'll use it to store blog articles and their vector embeddings, enabling semantic searches that find articles based on meaning rather than just keywords.",[27,48015,48016],{},"While alternatives exist—like PostgreSQL or paid services such as Supabase or Pinecone—these either require costly server resources or are paid services. This made SQLite our ideal choice.",[2855,48018,48020],{"id":48019},"why-do-we-need-a-vector-extension","Why Do We Need a Vector Extension?",[27,48022,48023,48024,48027],{},"Semantic searches require storing and querying vector embeddings. Since SQLite doesn't natively support vector operations, we'll use a platform-specific extension called ",[22,48025,48026],{},"sqlite-vec",". This enables SQLite to handle vector data and perform similarity searches.",[2855,48029,48031],{"id":48030},"installing-sqlite-and-the-vector-extension","Installing SQLite and the Vector Extension",[27,48033,48034],{},"First, install the SQLite dependency for Node.js:",[128,48036,48038],{"className":8665,"code":48037,"language":8667,"meta":133,"style":133},"npm install better-sqlite3\n",[22,48039,48040],{"__ignoreMap":133},[137,48041,48042,48044,48046],{"class":139,"line":140},[137,48043,9536],{"class":147},[137,48045,10268],{"class":284},[137,48047,48048],{"class":284}," better-sqlite3\n",[27,48050,48051],{},"Next, install the vector extension.",[3244,48053,48054],{},[27,48055,4737,48056,48058,48059,48062,48063,164,48066,164,48069,14528,48072,1017],{},[22,48057,48026],{}," package comes in different versions for various architectures. For Mac ARM, we'll use ",[22,48060,48061],{},"sqlite-vec-darwin-arm64",". Other options include: ",[22,48064,48065],{},"sqlite-vec-darwin-x64",[22,48067,48068],{},"sqlite-vec-linux-arm64",[22,48070,48071],{},"sqlite-vec-linux-x64",[22,48073,48074],{},"sqlite-vec-windows-x64",[27,48076,48077],{},"To simplify cross-platform support, here's a helper function that loads the appropriate vector extension based on your system:",[128,48079,48081],{"className":13299,"code":48080,"language":13301,"meta":133,"style":133},"function getPlatformSpecificExtension() {\n    const platform = process.platform;\n    const arch = process.arch;\n\n    if (platform === \"darwin\" && arch === \"arm64\") {\n        return {\n            packageName: \"sqlite-vec-darwin-arm64\",\n            extension: \"vec0.dylib\",\n        };\n    }\n\n    \u002F\u002F For Intel Macs\n    if (platform === \"darwin\") {\n        return {\n            packageName: \"sqlite-vec-darwin-x64\",\n            extension: \"vec0.dylib\",\n        };\n    }\n\n    if (platform === \"linux\" && arch === \"arm64\") {\n        return {\n            packageName: \"sqlite-vec-linux-arm64\",\n            extension: \"vec0.so\",\n        };\n    }\n\n    \u002F\u002F For Intel Linux\n    if (platform === \"linux\") {\n        return {\n            packageName: \"sqlite-vec-linux-x64\",\n            extension: \"vec0.so\",\n        };\n    }\n\n    return {\n        packageName: \"sqlite-vec-windows-x64\",\n        extension: \"vec0.dll\",\n    };\n}\n",[22,48082,48083,48092,48104,48116,48120,48144,48150,48160,48170,48174,48178,48182,48187,48199,48205,48214,48222,48226,48230,48234,48255,48261,48270,48279,48283,48287,48291,48296,48308,48314,48323,48331,48335,48339,48343,48349,48359,48369,48373],{"__ignoreMap":133},[137,48084,48085,48087,48090],{"class":139,"line":140},[137,48086,483],{"class":143},[137,48088,48089],{"class":147}," getPlatformSpecificExtension",[137,48091,275],{"class":157},[137,48093,48094,48096,48099,48101],{"class":139,"line":173},[137,48095,4177],{"class":143},[137,48097,48098],{"class":364}," platform",[137,48100,151],{"class":143},[137,48102,48103],{"class":157}," process.platform;\n",[137,48105,48106,48108,48111,48113],{"class":139,"line":188},[137,48107,4177],{"class":143},[137,48109,48110],{"class":364}," arch",[137,48112,151],{"class":143},[137,48114,48115],{"class":157}," process.arch;\n",[137,48117,48118],{"class":139,"line":269},[137,48119,516],{"emptyLinePlaceholder":515},[137,48121,48122,48124,48127,48129,48132,48134,48137,48139,48142],{"class":139,"line":278},[137,48123,24696],{"class":143},[137,48125,48126],{"class":157}," (platform ",[137,48128,5502],{"class":143},[137,48130,48131],{"class":284}," \"darwin\"",[137,48133,35905],{"class":143},[137,48135,48136],{"class":157}," arch ",[137,48138,5502],{"class":143},[137,48140,48141],{"class":284}," \"arm64\"",[137,48143,170],{"class":157},[137,48145,48146,48148],{"class":139,"line":291},[137,48147,5472],{"class":143},[137,48149,256],{"class":157},[137,48151,48152,48155,48158],{"class":139,"line":297},[137,48153,48154],{"class":157},"            packageName: ",[137,48156,48157],{"class":284},"\"sqlite-vec-darwin-arm64\"",[137,48159,1961],{"class":157},[137,48161,48162,48165,48168],{"class":139,"line":302},[137,48163,48164],{"class":157},"            extension: ",[137,48166,48167],{"class":284},"\"vec0.dylib\"",[137,48169,1961],{"class":157},[137,48171,48172],{"class":139,"line":662},[137,48173,1507],{"class":157},[137,48175,48176],{"class":139,"line":667},[137,48177,294],{"class":157},[137,48179,48180],{"class":139,"line":786},[137,48181,516],{"emptyLinePlaceholder":515},[137,48183,48184],{"class":139,"line":798},[137,48185,48186],{"class":308},"    \u002F\u002F For Intel Macs\n",[137,48188,48189,48191,48193,48195,48197],{"class":139,"line":803},[137,48190,24696],{"class":143},[137,48192,48126],{"class":157},[137,48194,5502],{"class":143},[137,48196,48131],{"class":284},[137,48198,170],{"class":157},[137,48200,48201,48203],{"class":139,"line":931},[137,48202,5472],{"class":143},[137,48204,256],{"class":157},[137,48206,48207,48209,48212],{"class":139,"line":1568},[137,48208,48154],{"class":157},[137,48210,48211],{"class":284},"\"sqlite-vec-darwin-x64\"",[137,48213,1961],{"class":157},[137,48215,48216,48218,48220],{"class":139,"line":1573},[137,48217,48164],{"class":157},[137,48219,48167],{"class":284},[137,48221,1961],{"class":157},[137,48223,48224],{"class":139,"line":1578},[137,48225,1507],{"class":157},[137,48227,48228],{"class":139,"line":1588},[137,48229,294],{"class":157},[137,48231,48232],{"class":139,"line":1601},[137,48233,516],{"emptyLinePlaceholder":515},[137,48235,48236,48238,48240,48242,48245,48247,48249,48251,48253],{"class":139,"line":3802},[137,48237,24696],{"class":143},[137,48239,48126],{"class":157},[137,48241,5502],{"class":143},[137,48243,48244],{"class":284}," \"linux\"",[137,48246,35905],{"class":143},[137,48248,48136],{"class":157},[137,48250,5502],{"class":143},[137,48252,48141],{"class":284},[137,48254,170],{"class":157},[137,48256,48257,48259],{"class":139,"line":3808},[137,48258,5472],{"class":143},[137,48260,256],{"class":157},[137,48262,48263,48265,48268],{"class":139,"line":3822},[137,48264,48154],{"class":157},[137,48266,48267],{"class":284},"\"sqlite-vec-linux-arm64\"",[137,48269,1961],{"class":157},[137,48271,48272,48274,48277],{"class":139,"line":3827},[137,48273,48164],{"class":157},[137,48275,48276],{"class":284},"\"vec0.so\"",[137,48278,1961],{"class":157},[137,48280,48281],{"class":139,"line":3832},[137,48282,1507],{"class":157},[137,48284,48285],{"class":139,"line":3840},[137,48286,294],{"class":157},[137,48288,48289],{"class":139,"line":3846},[137,48290,516],{"emptyLinePlaceholder":515},[137,48292,48293],{"class":139,"line":3861},[137,48294,48295],{"class":308},"    \u002F\u002F For Intel Linux\n",[137,48297,48298,48300,48302,48304,48306],{"class":139,"line":3883},[137,48299,24696],{"class":143},[137,48301,48126],{"class":157},[137,48303,5502],{"class":143},[137,48305,48244],{"class":284},[137,48307,170],{"class":157},[137,48309,48310,48312],{"class":139,"line":3896},[137,48311,5472],{"class":143},[137,48313,256],{"class":157},[137,48315,48316,48318,48321],{"class":139,"line":3901},[137,48317,48154],{"class":157},[137,48319,48320],{"class":284},"\"sqlite-vec-linux-x64\"",[137,48322,1961],{"class":157},[137,48324,48325,48327,48329],{"class":139,"line":3906},[137,48326,48164],{"class":157},[137,48328,48276],{"class":284},[137,48330,1961],{"class":157},[137,48332,48333],{"class":139,"line":3911},[137,48334,1507],{"class":157},[137,48336,48337],{"class":139,"line":4666},[137,48338,294],{"class":157},[137,48340,48341],{"class":139,"line":4672},[137,48342,516],{"emptyLinePlaceholder":515},[137,48344,48345,48347],{"class":139,"line":4680},[137,48346,176],{"class":143},[137,48348,256],{"class":157},[137,48350,48351,48354,48357],{"class":139,"line":4711},[137,48352,48353],{"class":157},"        packageName: ",[137,48355,48356],{"class":284},"\"sqlite-vec-windows-x64\"",[137,48358,1961],{"class":157},[137,48360,48361,48364,48367],{"class":139,"line":4716},[137,48362,48363],{"class":157},"        extension: ",[137,48365,48366],{"class":284},"\"vec0.dll\"",[137,48368,1961],{"class":157},[137,48370,48371],{"class":139,"line":4721},[137,48372,1892],{"class":157},[137,48374,48375],{"class":139,"line":4727},[137,48376,510],{"class":157},[27,48378,48379],{},"This ensures the correct extension loads for your system, enabling vector operations in SQLite.",[104,48381,48383],{"id":48382},"create-database-and-import-articles-into-the-sqlite-database","Create Database and Import Articles into the SQLite Database",[27,48385,48386,48387,48390,48391,48393,48394,1017],{},"To power our AI search engine, we need a database that stores articles and their embeddings. We'll create a SQLite database in the project root and populate it with data from ",[22,48388,48389],{},"articles-embeddings.json",", which contains our blog articles and their embeddings—generated using Hugging Face's ",[22,48392,28722],{}," model through ",[22,48395,48396],{},"transformers.js",[123,48398,48400],{"id":48399},"the-script-to-create-the-database","The Script to Create the Database",[27,48402,48403,48404,894],{},"Here's the script that creates the database, defines a virtual table for storing articles and embeddings, and imports data from ",[22,48405,48389],{},[128,48407,48409],{"className":13299,"code":48408,"language":13301,"meta":133,"style":133},"import * as sqliteVec from \"sqlite-vec\";\nimport DatabaseSync from \"better-sqlite3\";\nimport fs from \"fs\";\nimport path from \"path\";\n\ninterface Article {\n    id?: number;\n    articlePath: string;\n    embeddings: number[];\n    content: string;\n}\n\nconst dbFilePath = \"blog_articles.sqlite3\";\n\nexport function createDatabase() {\n    const articlesEmbeddings = JSON.parse(fs.readFileSync(path.resolve(\".\u002Farticles-embeddings.json\"), \"utf-8\"));\n\n    \u002F\u002F Ensure the database file doesn't exist before creating a new one\n    if (fs.existsSync(dbFilePath)) {\n        fs.unlinkSync(dbFilePath); \u002F\u002F Delete the existing file\n    }\n\n    const db = new DatabaseSync(dbFilePath, { allowExtension: true });\n    sqliteVec.load(db);\n\n    const items = articlesEmbeddings.map((article: Article, index: number) => {\n        const blobData = new Uint8Array(new Float32Array(article.embeddings as number[]).buffer);\n        return {\n            id: index + 1,\n            embeddings: blobData,\n            content: article.content,\n            articlePath: article.articlePath,\n        };\n    }) as Article[];\n\n    const vectorDimension = articlesEmbeddings.length > 0 ? articlesEmbeddings[0].embeddings.length : 384;\n\n    \u002F\u002F Create the virtual table with proper vector syntax\n    db.exec(`CREATE VIRTUAL TABLE blog_articles USING vec0(\n        content TEXT,\n        articlePath TEXT,\n        embedding float[${vectorDimension}]\n    )`);\n\n    \u002F\u002F Insert the items into the database\n    for (const item of items) {\n        const itemId = Number.isInteger(item.id) ? item.id : Math.floor(Number(item.id));\n\n        \u002F\u002F Escape single quotes in content to prevent SQL injection\n        const safeContent = item.content.replace(\u002F'\u002Fg, \"''\");\n        const safeArticlePath = item.articlePath.replace(\u002F'\u002Fg, \"''\");\n\n        db.exec(\n            `INSERT INTO blog_articles(rowid, content, articlePath, embedding)\n             VALUES (${itemId}, '${safeContent}', '${safeArticlePath}', x'${Buffer.from(item.embeddings).toString(\n                 \"hex\"\n             )}')`\n        );\n    }\n\n    db.close();\n}\n\ncreateDatabase();\n",[22,48410,48411,48429,48443,48457,48471,48475,48485,48495,48506,48517,48528,48532,48536,48550,48554,48565,48604,48608,48613,48626,48640,48644,48648,48669,48680,48684,48722,48753,48759,48770,48775,48780,48785,48789,48800,48804,48841,48845,48850,48863,48868,48873,48884,48891,48895,48900,48917,48953,48957,48962,48994,49024,49028,49037,49042,49088,49093,49101,49106,49110,49114,49122,49126,49130],{"__ignoreMap":133},[137,48412,48413,48415,48417,48419,48422,48424,48427],{"class":139,"line":140},[137,48414,10287],{"class":143},[137,48416,13878],{"class":364},[137,48418,13881],{"class":143},[137,48420,48421],{"class":157}," sqliteVec ",[137,48423,10954],{"class":143},[137,48425,48426],{"class":284}," \"sqlite-vec\"",[137,48428,3276],{"class":157},[137,48430,48431,48433,48436,48438,48441],{"class":139,"line":173},[137,48432,10287],{"class":143},[137,48434,48435],{"class":157}," DatabaseSync ",[137,48437,10954],{"class":143},[137,48439,48440],{"class":284}," \"better-sqlite3\"",[137,48442,3276],{"class":157},[137,48444,48445,48447,48450,48452,48455],{"class":139,"line":188},[137,48446,10287],{"class":143},[137,48448,48449],{"class":157}," fs ",[137,48451,10954],{"class":143},[137,48453,48454],{"class":284}," \"fs\"",[137,48456,3276],{"class":157},[137,48458,48459,48461,48464,48466,48469],{"class":139,"line":269},[137,48460,10287],{"class":143},[137,48462,48463],{"class":157}," path ",[137,48465,10954],{"class":143},[137,48467,48468],{"class":284}," \"path\"",[137,48470,3276],{"class":157},[137,48472,48473],{"class":139,"line":278},[137,48474,516],{"emptyLinePlaceholder":515},[137,48476,48477,48480,48483],{"class":139,"line":291},[137,48478,48479],{"class":143},"interface",[137,48481,48482],{"class":147}," Article",[137,48484,256],{"class":157},[137,48486,48487,48489,48491,48493],{"class":139,"line":297},[137,48488,31390],{"class":161},[137,48490,35824],{"class":143},[137,48492,31395],{"class":364},[137,48494,3276],{"class":157},[137,48496,48497,48500,48502,48504],{"class":139,"line":302},[137,48498,48499],{"class":161},"    articlePath",[137,48501,894],{"class":143},[137,48503,13630],{"class":364},[137,48505,3276],{"class":157},[137,48507,48508,48511,48513,48515],{"class":139,"line":662},[137,48509,48510],{"class":161},"    embeddings",[137,48512,894],{"class":143},[137,48514,31395],{"class":364},[137,48516,14968],{"class":157},[137,48518,48519,48522,48524,48526],{"class":139,"line":667},[137,48520,48521],{"class":161},"    content",[137,48523,894],{"class":143},[137,48525,13630],{"class":364},[137,48527,3276],{"class":157},[137,48529,48530],{"class":139,"line":786},[137,48531,510],{"class":157},[137,48533,48534],{"class":139,"line":798},[137,48535,516],{"emptyLinePlaceholder":515},[137,48537,48538,48540,48543,48545,48548],{"class":139,"line":803},[137,48539,3077],{"class":143},[137,48541,48542],{"class":364}," dbFilePath",[137,48544,151],{"class":143},[137,48546,48547],{"class":284}," \"blog_articles.sqlite3\"",[137,48549,3276],{"class":157},[137,48551,48552],{"class":139,"line":931},[137,48553,516],{"emptyLinePlaceholder":515},[137,48555,48556,48558,48560,48563],{"class":139,"line":1568},[137,48557,13456],{"class":143},[137,48559,154],{"class":143},[137,48561,48562],{"class":147}," createDatabase",[137,48564,275],{"class":157},[137,48566,48567,48569,48572,48574,48576,48578,48580,48583,48586,48589,48592,48594,48597,48599,48602],{"class":139,"line":1573},[137,48568,4177],{"class":143},[137,48570,48571],{"class":364}," articlesEmbeddings",[137,48573,151],{"class":143},[137,48575,17436],{"class":364},[137,48577,1017],{"class":157},[137,48579,17441],{"class":147},[137,48581,48582],{"class":157},"(fs.",[137,48584,48585],{"class":147},"readFileSync",[137,48587,48588],{"class":157},"(path.",[137,48590,48591],{"class":147},"resolve",[137,48593,356],{"class":157},[137,48595,48596],{"class":284},"\".\u002Farticles-embeddings.json\"",[137,48598,2214],{"class":157},[137,48600,48601],{"class":284},"\"utf-8\"",[137,48603,8614],{"class":157},[137,48605,48606],{"class":139,"line":1578},[137,48607,516],{"emptyLinePlaceholder":515},[137,48609,48610],{"class":139,"line":1588},[137,48611,48612],{"class":308},"    \u002F\u002F Ensure the database file doesn't exist before creating a new one\n",[137,48614,48615,48617,48620,48623],{"class":139,"line":1601},[137,48616,24696],{"class":143},[137,48618,48619],{"class":157}," (fs.",[137,48621,48622],{"class":147},"existsSync",[137,48624,48625],{"class":157},"(dbFilePath)) {\n",[137,48627,48628,48631,48634,48637],{"class":139,"line":3802},[137,48629,48630],{"class":157},"        fs.",[137,48632,48633],{"class":147},"unlinkSync",[137,48635,48636],{"class":157},"(dbFilePath); ",[137,48638,48639],{"class":308},"\u002F\u002F Delete the existing file\n",[137,48641,48642],{"class":139,"line":3808},[137,48643,294],{"class":157},[137,48645,48646],{"class":139,"line":3822},[137,48647,516],{"emptyLinePlaceholder":515},[137,48649,48650,48652,48655,48657,48659,48662,48665,48667],{"class":139,"line":3827},[137,48651,4177],{"class":143},[137,48653,48654],{"class":364}," db",[137,48656,151],{"class":143},[137,48658,1426],{"class":143},[137,48660,48661],{"class":147}," DatabaseSync",[137,48663,48664],{"class":157},"(dbFilePath, { allowExtension: ",[137,48666,3097],{"class":364},[137,48668,4168],{"class":157},[137,48670,48671,48674,48677],{"class":139,"line":3832},[137,48672,48673],{"class":157},"    sqliteVec.",[137,48675,48676],{"class":147},"load",[137,48678,48679],{"class":157},"(db);\n",[137,48681,48682],{"class":139,"line":3840},[137,48683,516],{"emptyLinePlaceholder":515},[137,48685,48686,48688,48691,48693,48696,48698,48700,48703,48705,48707,48709,48712,48714,48716,48718,48720],{"class":139,"line":3846},[137,48687,4177],{"class":143},[137,48689,48690],{"class":364}," items",[137,48692,151],{"class":143},[137,48694,48695],{"class":157}," articlesEmbeddings.",[137,48697,37476],{"class":147},[137,48699,2774],{"class":157},[137,48701,48702],{"class":161},"article",[137,48704,894],{"class":143},[137,48706,48482],{"class":147},[137,48708,164],{"class":157},[137,48710,48711],{"class":161},"index",[137,48713,894],{"class":143},[137,48715,31395],{"class":364},[137,48717,219],{"class":157},[137,48719,222],{"class":143},[137,48721,256],{"class":157},[137,48723,48724,48726,48729,48731,48733,48736,48738,48740,48743,48746,48748,48750],{"class":139,"line":3861},[137,48725,3008],{"class":143},[137,48727,48728],{"class":364}," blobData",[137,48730,151],{"class":143},[137,48732,1426],{"class":143},[137,48734,48735],{"class":147}," Uint8Array",[137,48737,356],{"class":157},[137,48739,1361],{"class":143},[137,48741,48742],{"class":147}," Float32Array",[137,48744,48745],{"class":157},"(article.embeddings ",[137,48747,24431],{"class":143},[137,48749,31395],{"class":364},[137,48751,48752],{"class":157},"[]).buffer);\n",[137,48754,48755,48757],{"class":139,"line":3883},[137,48756,5472],{"class":143},[137,48758,256],{"class":157},[137,48760,48761,48764,48766,48768],{"class":139,"line":3896},[137,48762,48763],{"class":157},"            id: index ",[137,48765,182],{"class":143},[137,48767,8030],{"class":364},[137,48769,1961],{"class":157},[137,48771,48772],{"class":139,"line":3901},[137,48773,48774],{"class":157},"            embeddings: blobData,\n",[137,48776,48777],{"class":139,"line":3906},[137,48778,48779],{"class":157},"            content: article.content,\n",[137,48781,48782],{"class":139,"line":3911},[137,48783,48784],{"class":157},"            articlePath: article.articlePath,\n",[137,48786,48787],{"class":139,"line":4666},[137,48788,1507],{"class":157},[137,48790,48791,48794,48796,48798],{"class":139,"line":4672},[137,48792,48793],{"class":157},"    }) ",[137,48795,24431],{"class":143},[137,48797,48482],{"class":147},[137,48799,14968],{"class":157},[137,48801,48802],{"class":139,"line":4680},[137,48803,516],{"emptyLinePlaceholder":515},[137,48805,48806,48808,48811,48813,48815,48817,48820,48822,48824,48827,48829,48832,48834,48836,48839],{"class":139,"line":4711},[137,48807,4177],{"class":143},[137,48809,48810],{"class":364}," vectorDimension",[137,48812,151],{"class":143},[137,48814,48695],{"class":157},[137,48816,8611],{"class":364},[137,48818,48819],{"class":143}," >",[137,48821,7687],{"class":364},[137,48823,26196],{"class":143},[137,48825,48826],{"class":157}," articlesEmbeddings[",[137,48828,6044],{"class":364},[137,48830,48831],{"class":157},"].embeddings.",[137,48833,8611],{"class":364},[137,48835,26201],{"class":143},[137,48837,48838],{"class":364}," 384",[137,48840,3276],{"class":157},[137,48842,48843],{"class":139,"line":4716},[137,48844,516],{"emptyLinePlaceholder":515},[137,48846,48847],{"class":139,"line":4721},[137,48848,48849],{"class":308},"    \u002F\u002F Create the virtual table with proper vector syntax\n",[137,48851,48852,48855,48858,48860],{"class":139,"line":4727},[137,48853,48854],{"class":157},"    db.",[137,48856,48857],{"class":147},"exec",[137,48859,356],{"class":157},[137,48861,48862],{"class":284},"`CREATE VIRTUAL TABLE blog_articles USING vec0(\n",[137,48864,48865],{"class":139,"line":4732},[137,48866,48867],{"class":284},"        content TEXT,\n",[137,48869,48870],{"class":139,"line":5006},[137,48871,48872],{"class":284},"        articlePath TEXT,\n",[137,48874,48875,48878,48881],{"class":139,"line":5014},[137,48876,48877],{"class":284},"        embedding float[${",[137,48879,48880],{"class":157},"vectorDimension",[137,48882,48883],{"class":284},"}]\n",[137,48885,48886,48889],{"class":139,"line":14343},[137,48887,48888],{"class":284},"    )`",[137,48890,1502],{"class":157},[137,48892,48893],{"class":139,"line":24199},[137,48894,516],{"emptyLinePlaceholder":515},[137,48896,48897],{"class":139,"line":24773},[137,48898,48899],{"class":308},"    \u002F\u002F Insert the items into the database\n",[137,48901,48902,48904,48906,48908,48911,48914],{"class":139,"line":24778},[137,48903,21473],{"class":143},[137,48905,158],{"class":157},[137,48907,3077],{"class":143},[137,48909,48910],{"class":364}," item",[137,48912,48913],{"class":143}," of",[137,48915,48916],{"class":157}," items) {\n",[137,48918,48919,48921,48924,48926,48929,48932,48935,48937,48940,48942,48944,48946,48948,48950],{"class":139,"line":24783},[137,48920,3008],{"class":143},[137,48922,48923],{"class":364}," itemId",[137,48925,151],{"class":143},[137,48927,48928],{"class":157}," Number.",[137,48930,48931],{"class":147},"isInteger",[137,48933,48934],{"class":157},"(item.id) ",[137,48936,12972],{"class":143},[137,48938,48939],{"class":157}," item.id ",[137,48941,894],{"class":143},[137,48943,7675],{"class":157},[137,48945,8011],{"class":147},[137,48947,356],{"class":157},[137,48949,45027],{"class":147},[137,48951,48952],{"class":157},"(item.id));\n",[137,48954,48955],{"class":139,"line":24793},[137,48956,516],{"emptyLinePlaceholder":515},[137,48958,48959],{"class":139,"line":24827},[137,48960,48961],{"class":308},"        \u002F\u002F Escape single quotes in content to prevent SQL injection\n",[137,48963,48964,48966,48969,48971,48974,48976,48978,48980,48983,48985,48987,48989,48992],{"class":139,"line":24857},[137,48965,3008],{"class":143},[137,48967,48968],{"class":364}," safeContent",[137,48970,151],{"class":143},[137,48972,48973],{"class":157}," item.content.",[137,48975,33919],{"class":147},[137,48977,356],{"class":157},[137,48979,47],{"class":284},[137,48981,48982],{"class":14746},"'",[137,48984,47],{"class":284},[137,48986,33931],{"class":143},[137,48988,164],{"class":157},[137,48990,48991],{"class":284},"\"''\"",[137,48993,1502],{"class":157},[137,48995,48996,48998,49001,49003,49006,49008,49010,49012,49014,49016,49018,49020,49022],{"class":139,"line":24862},[137,48997,3008],{"class":143},[137,48999,49000],{"class":364}," safeArticlePath",[137,49002,151],{"class":143},[137,49004,49005],{"class":157}," item.articlePath.",[137,49007,33919],{"class":147},[137,49009,356],{"class":157},[137,49011,47],{"class":284},[137,49013,48982],{"class":14746},[137,49015,47],{"class":284},[137,49017,33931],{"class":143},[137,49019,164],{"class":157},[137,49021,48991],{"class":284},[137,49023,1502],{"class":157},[137,49025,49026],{"class":139,"line":24867},[137,49027,516],{"emptyLinePlaceholder":515},[137,49029,49030,49033,49035],{"class":139,"line":24884},[137,49031,49032],{"class":157},"        db.",[137,49034,48857],{"class":147},[137,49036,11813],{"class":157},[137,49038,49039],{"class":139,"line":24892},[137,49040,49041],{"class":284},"            `INSERT INTO blog_articles(rowid, content, articlePath, embedding)\n",[137,49043,49044,49047,49050,49053,49056,49059,49062,49065,49068,49070,49072,49074,49077,49079,49082,49084,49086],{"class":139,"line":24902},[137,49045,49046],{"class":284},"             VALUES (${",[137,49048,49049],{"class":157},"itemId",[137,49051,49052],{"class":284},"}, '${",[137,49054,49055],{"class":157},"safeContent",[137,49057,49058],{"class":284},"}', '${",[137,49060,49061],{"class":157},"safeArticlePath",[137,49063,49064],{"class":284},"}', x'${",[137,49066,49067],{"class":157},"Buffer",[137,49069,1017],{"class":284},[137,49071,10954],{"class":147},[137,49073,356],{"class":284},[137,49075,49076],{"class":157},"item",[137,49078,1017],{"class":284},[137,49080,49081],{"class":157},"embeddings",[137,49083,4409],{"class":284},[137,49085,7692],{"class":147},[137,49087,11813],{"class":284},[137,49089,49090],{"class":139,"line":24925},[137,49091,49092],{"class":284},"                 \"hex\"\n",[137,49094,49095,49098],{"class":139,"line":24933},[137,49096,49097],{"class":284},"             )",[137,49099,49100],{"class":284},"}')`\n",[137,49102,49103],{"class":139,"line":24941},[137,49104,49105],{"class":157},"        );\n",[137,49107,49108],{"class":139,"line":24952},[137,49109,294],{"class":157},[137,49111,49112],{"class":139,"line":24960},[137,49113,516],{"emptyLinePlaceholder":515},[137,49115,49116,49118,49120],{"class":139,"line":24982},[137,49117,48854],{"class":157},[137,49119,31745],{"class":147},[137,49121,924],{"class":157},[137,49123,49124],{"class":139,"line":24989},[137,49125,510],{"class":157},[137,49127,49128],{"class":139,"line":24994},[137,49129,516],{"emptyLinePlaceholder":515},[137,49131,49132,49135],{"class":139,"line":24999},[137,49133,49134],{"class":147},"createDatabase",[137,49136,924],{"class":157},[27,49138,49139,49140,49142],{},"For a simpler development workflow, you can use the ",[22,49141,29200],{}," package to run TypeScript files directly instead of compiling to JavaScript first.",[27,49144,49145,49146,49148],{},"Install ",[22,49147,29200],{}," as a development dependency:",[128,49150,49152],{"className":8665,"code":49151,"language":8667,"meta":133,"style":133},"npm install tsx --save-dev\n",[22,49153,49154],{"__ignoreMap":133},[137,49155,49156,49158,49160,49163],{"class":139,"line":140},[137,49157,9536],{"class":147},[137,49159,10268],{"class":284},[137,49161,49162],{"class":284}," tsx",[137,49164,49165],{"class":364}," --save-dev\n",[27,49167,49168,49169,49172],{},"Then run your ",[22,49170,49171],{},"createDatabase.ts"," script:",[128,49174,49176],{"className":8665,"code":49175,"language":8667,"meta":133,"style":133},"npx tsx createDatabase.ts\n",[22,49177,49178],{"__ignoreMap":133},[137,49179,49180,49182,49184],{"class":139,"line":140},[137,49181,21167],{"class":147},[137,49183,49162],{"class":284},[137,49185,49186],{"class":284}," createDatabase.ts\n",[27,49188,49189,49190,49193],{},"This command generates a ",[22,49191,49192],{},"blog_articles.sqlite3"," file in your project's root directory. The resulting SQLite database—complete with your articles and their embeddings—is now ready to power your AI search engine.",[104,49195,49197],{"id":49196},"retrieving-relevant-data-from-the-database-using-user-queries","Retrieving Relevant Data from the Database Using User Queries",[27,49199,49200,49201,49203,49204,49206],{},"We use Hugging Face's ",[22,49202,48396],{}," library with our local ",[22,49205,28722],{}," model to generate embeddings.",[27,49208,49209],{},"First, install the transformers package:",[128,49211,49212],{"className":8665,"code":46266,"language":8667,"meta":133,"style":133},[22,49213,49214],{"__ignoreMap":133},[137,49215,49216,49218,49220],{"class":139,"line":140},[137,49217,9536],{"class":147},[137,49219,10268],{"class":284},[137,49221,46277],{"class":284},[27,49223,49224],{},"Here's how it works in our project:",[27,49226,49227],{},"Load a local model for embedding generation from a user query:",[128,49229,49231],{"className":13299,"code":49230,"language":13301,"meta":133,"style":133},"const embeddingsGenerator = await pipeline(\"feature-extraction\", \".\u002Flocal_models\u002Fall-MiniLM-L6-v2\u002F\", {\n    local_files_only: true,\n});\n",[22,49232,49233,49257,49266],{"__ignoreMap":133},[137,49234,49235,49237,49240,49242,49244,49246,49248,49250,49252,49255],{"class":139,"line":140},[137,49236,3077],{"class":143},[137,49238,49239],{"class":364}," embeddingsGenerator",[137,49241,151],{"class":143},[137,49243,15069],{"class":143},[137,49245,46334],{"class":147},[137,49247,356],{"class":157},[137,49249,46339],{"class":284},[137,49251,164],{"class":157},[137,49253,49254],{"class":284},"\".\u002Flocal_models\u002Fall-MiniLM-L6-v2\u002F\"",[137,49256,5396],{"class":157},[137,49258,49259,49262,49264],{"class":139,"line":173},[137,49260,49261],{"class":157},"    local_files_only: ",[137,49263,3097],{"class":364},[137,49265,1961],{"class":157},[137,49267,49268],{"class":139,"line":188},[137,49269,5422],{"class":157},[27,49271,49272],{},"Generate embeddings for a query:",[128,49274,49276],{"className":13299,"code":49275,"language":13301,"meta":133,"style":133},"const embeddingsOutput = await embeddingsGenerator(query, { pooling: \"mean\", normalize: true });\nconst vector = embeddingsOutput.tolist();\n",[22,49277,49278,49302],{"__ignoreMap":133},[137,49279,49280,49282,49285,49287,49289,49291,49294,49296,49298,49300],{"class":139,"line":140},[137,49281,3077],{"class":143},[137,49283,49284],{"class":364}," embeddingsOutput",[137,49286,151],{"class":143},[137,49288,15069],{"class":143},[137,49290,49239],{"class":147},[137,49292,49293],{"class":157},"(query, { pooling: ",[137,49295,46365],{"class":284},[137,49297,46368],{"class":157},[137,49299,3097],{"class":364},[137,49301,4168],{"class":157},[137,49303,49304,49306,49308,49310,49313,49315],{"class":139,"line":173},[137,49305,3077],{"class":143},[137,49307,46379],{"class":364},[137,49309,151],{"class":143},[137,49311,49312],{"class":157}," embeddingsOutput.",[137,49314,46387],{"class":147},[137,49316,924],{"class":157},[27,49318,49319],{},"Use these embeddings to perform a semantic search in the SQLite database:",[128,49321,49323],{"className":13299,"code":49322,"language":13301,"meta":133,"style":133},"const rows = db\n    .prepare(\n        `\n    SELECT rowid, distance, content, articlePath\n    FROM blog_articles\n    WHERE embedding MATCH ?\n    ORDER BY distance\n    LIMIT 3\n`\n    )\n    .all(new Uint8Array(new Float32Array(vector[0]).buffer));\n",[22,49324,49325,49337,49346,49351,49356,49361,49366,49371,49376,49380,49385],{"__ignoreMap":133},[137,49326,49327,49329,49332,49334],{"class":139,"line":140},[137,49328,3077],{"class":143},[137,49330,49331],{"class":364}," rows",[137,49333,151],{"class":143},[137,49335,49336],{"class":157}," db\n",[137,49338,49339,49341,49344],{"class":139,"line":173},[137,49340,2748],{"class":157},[137,49342,49343],{"class":147},"prepare",[137,49345,11813],{"class":157},[137,49347,49348],{"class":139,"line":188},[137,49349,49350],{"class":284},"        `\n",[137,49352,49353],{"class":139,"line":269},[137,49354,49355],{"class":284},"    SELECT rowid, distance, content, articlePath\n",[137,49357,49358],{"class":139,"line":278},[137,49359,49360],{"class":284},"    FROM blog_articles\n",[137,49362,49363],{"class":139,"line":291},[137,49364,49365],{"class":284},"    WHERE embedding MATCH ?\n",[137,49367,49368],{"class":139,"line":297},[137,49369,49370],{"class":284},"    ORDER BY distance\n",[137,49372,49373],{"class":139,"line":302},[137,49374,49375],{"class":284},"    LIMIT 3\n",[137,49377,49378],{"class":139,"line":662},[137,49379,22062],{"class":284},[137,49381,49382],{"class":139,"line":667},[137,49383,49384],{"class":157},"    )\n",[137,49386,49387,49389,49392,49394,49396,49398,49400,49402,49404,49406,49408],{"class":139,"line":786},[137,49388,2748],{"class":157},[137,49390,49391],{"class":147},"all",[137,49393,356],{"class":157},[137,49395,1361],{"class":143},[137,49397,48735],{"class":147},[137,49399,356],{"class":157},[137,49401,1361],{"class":143},[137,49403,48742],{"class":147},[137,49405,46398],{"class":157},[137,49407,6044],{"class":364},[137,49409,49410],{"class":157},"]).buffer));\n",[27,49412,49413],{},"This process retrieves the three most relevant articles based on the query.",[104,49415,49417],{"id":49416},"generate-a-response","Generate a Response",[27,49419,49420,49421,49423],{},"For generating responses, we use our other local model, ",[22,49422,47949],{},". First, we load the model:",[128,49425,49427],{"className":13299,"code":49426,"language":13301,"meta":133,"style":133},"const textGenerator = await pipeline(\"text-generation\", \".\u002Flocal_models\u002FPhi-3-mini-4k-instruct\u002F\", {\n    local_files_only: true,\n});\n",[22,49428,49429,49454,49462],{"__ignoreMap":133},[137,49430,49431,49433,49436,49438,49440,49442,49444,49447,49449,49452],{"class":139,"line":140},[137,49432,3077],{"class":143},[137,49434,49435],{"class":364}," textGenerator",[137,49437,151],{"class":143},[137,49439,15069],{"class":143},[137,49441,46334],{"class":147},[137,49443,356],{"class":157},[137,49445,49446],{"class":284},"\"text-generation\"",[137,49448,164],{"class":157},[137,49450,49451],{"class":284},"\".\u002Flocal_models\u002FPhi-3-mini-4k-instruct\u002F\"",[137,49453,5396],{"class":157},[137,49455,49456,49458,49460],{"class":139,"line":173},[137,49457,49261],{"class":157},[137,49459,3097],{"class":364},[137,49461,1961],{"class":157},[137,49463,49464],{"class":139,"line":188},[137,49465,5422],{"class":157},[27,49467,49468],{},"We prepare a prompt that guides the text generation model. The system message includes:",[2569,49470,49471,49477,49483],{},[1006,49472,49473,49476],{},[42,49474,49475],{},"Context",": The AI's role and knowledge base",[1006,49478,49479,49482],{},[42,49480,49481],{},"Relevant Articles",": Top three search results with sources",[1006,49484,49485,49488,49489,49491,49492,49494,49495,49497],{},[42,49486,49487],{},"Instructions",": Guidelines for generating responses, including:",[45848,49490],{},"\na. Using only retrieved content ",[45848,49493],{},"\nb. Avoiding unsupported claims ",[45848,49496],{},"\nc. Following formatting requirements",[27,49499,49500],{},"The model receives this prompt through a messages array containing the system message and user query, generating an appropriate response with controlled length and creativity parameters.",[128,49502,49504],{"className":13299,"code":49503,"language":13301,"meta":133,"style":133},"const systemMessage = `You are an AI assistant helping users by generating accurate and well-structured responses based on retrieved knowledge. \nBelow are the top three most relevant content pieces retrieved from an AI-powered search engine using semantic embeddings. \nUse them as context to generate a clear, concise, and helpful response to the user's query.\n\nHere are the top three most relevant content pieces retrieved from the AI-powered search engine:\n\n${results\n    .map((article, index: number) => {\n        return `${index + 1}. ${article.content}\n        \n        Source: ${\"https:\u002F\u002Fwww.trpkovsi.com\" + article.articlePath}`;\n    })\n    .join(\"\\n\")}\n\nBased on the user query, generate a response using the retrieved content.\n\nInstructions:\n- Only use information that is explicitly mentioned in the retrieved content above.\n- Summarise and synthesise the retrieved content to generate a helpful answer.\n- Maintain a technical and informative tone.\n- Do NOT make up any facts or information that isn't in the retrieved content.\n- If the retrieved content doesn't contain information relevant to the query, respond with \"I don't have information about that topic in my knowledge base\" - don't try to provide a general response.\n- If the retrieved content only partially addresses the query, only answer what's supported by the content and acknowledge the limitations.\n- Use Australian English spelling and grammar.\n- Ensure the response is in Markdown format.\n- Cite sources inline where applicable using Markdown links to the provided URLs.\n- Do not add a separate \"Sources\" section; instead, reference them within the relevant parts of the response.`;\n\nconst messages = [\n    {\n        role: \"system\",\n        content: systemMessage,\n    },\n    { role: \"user\", content: `User query: \"${query}\"` },\n];\n\nconst response = await textGenerator(messages, {\n    max_new_tokens: 4096,\n    temperature: 0.2,\n});\n",[22,49505,49506,49518,49523,49528,49532,49537,49541,49549,49573,49596,49601,49620,49624,49643,49647,49652,49656,49661,49666,49671,49676,49681,49686,49691,49696,49701,49706,49713,49717,49729,49733,49743,49748,49752,49773,49777,49781,49797,49807,49816],{"__ignoreMap":133},[137,49507,49508,49510,49513,49515],{"class":139,"line":140},[137,49509,3077],{"class":143},[137,49511,49512],{"class":364}," systemMessage",[137,49514,151],{"class":143},[137,49516,49517],{"class":284}," `You are an AI assistant helping users by generating accurate and well-structured responses based on retrieved knowledge. \n",[137,49519,49520],{"class":139,"line":173},[137,49521,49522],{"class":284},"Below are the top three most relevant content pieces retrieved from an AI-powered search engine using semantic embeddings. \n",[137,49524,49525],{"class":139,"line":188},[137,49526,49527],{"class":284},"Use them as context to generate a clear, concise, and helpful response to the user's query.\n",[137,49529,49530],{"class":139,"line":269},[137,49531,516],{"emptyLinePlaceholder":515},[137,49533,49534],{"class":139,"line":278},[137,49535,49536],{"class":284},"Here are the top three most relevant content pieces retrieved from the AI-powered search engine:\n",[137,49538,49539],{"class":139,"line":291},[137,49540,516],{"emptyLinePlaceholder":515},[137,49542,49543,49546],{"class":139,"line":297},[137,49544,49545],{"class":284},"${",[137,49547,49548],{"class":157},"results\n",[137,49550,49551,49553,49555,49557,49559,49561,49563,49565,49567,49569,49571],{"class":139,"line":302},[137,49552,2748],{"class":284},[137,49554,37476],{"class":147},[137,49556,2774],{"class":284},[137,49558,48702],{"class":364},[137,49560,164],{"class":284},[137,49562,48711],{"class":364},[137,49564,894],{"class":143},[137,49566,31395],{"class":364},[137,49568,219],{"class":284},[137,49570,222],{"class":143},[137,49572,256],{"class":284},[137,49574,49575,49577,49579,49581,49583,49585,49588,49590,49592,49594],{"class":139,"line":662},[137,49576,5472],{"class":143},[137,49578,4686],{"class":284},[137,49580,48711],{"class":157},[137,49582,361],{"class":143},[137,49584,8030],{"class":364},[137,49586,49587],{"class":284},"}. ${",[137,49589,48702],{"class":157},[137,49591,1017],{"class":284},[137,49593,29728],{"class":157},[137,49595,510],{"class":284},[137,49597,49598],{"class":139,"line":667},[137,49599,49600],{"class":284},"        \n",[137,49602,49603,49606,49608,49611,49613,49616,49618],{"class":139,"line":786},[137,49604,49605],{"class":284},"        Source: ${\"https:\u002F\u002Fwww.trpkovsi.com\"",[137,49607,361],{"class":143},[137,49609,49610],{"class":157}," article",[137,49612,1017],{"class":284},[137,49614,49615],{"class":157},"articlePath",[137,49617,4706],{"class":284},[137,49619,3276],{"class":284},[137,49621,49622],{"class":139,"line":798},[137,49623,2800],{"class":284},[137,49625,49626,49628,49630,49632,49634,49637,49639,49641],{"class":139,"line":803},[137,49627,2748],{"class":284},[137,49629,8628],{"class":147},[137,49631,356],{"class":284},[137,49633,28792],{"class":284},[137,49635,49636],{"class":364},"\\n",[137,49638,28792],{"class":284},[137,49640,14105],{"class":284},[137,49642,510],{"class":284},[137,49644,49645],{"class":139,"line":931},[137,49646,516],{"emptyLinePlaceholder":515},[137,49648,49649],{"class":139,"line":1568},[137,49650,49651],{"class":284},"Based on the user query, generate a response using the retrieved content.\n",[137,49653,49654],{"class":139,"line":1573},[137,49655,516],{"emptyLinePlaceholder":515},[137,49657,49658],{"class":139,"line":1578},[137,49659,49660],{"class":284},"Instructions:\n",[137,49662,49663],{"class":139,"line":1588},[137,49664,49665],{"class":284},"- Only use information that is explicitly mentioned in the retrieved content above.\n",[137,49667,49668],{"class":139,"line":1601},[137,49669,49670],{"class":284},"- Summarise and synthesise the retrieved content to generate a helpful answer.\n",[137,49672,49673],{"class":139,"line":3802},[137,49674,49675],{"class":284},"- Maintain a technical and informative tone.\n",[137,49677,49678],{"class":139,"line":3808},[137,49679,49680],{"class":284},"- Do NOT make up any facts or information that isn't in the retrieved content.\n",[137,49682,49683],{"class":139,"line":3822},[137,49684,49685],{"class":284},"- If the retrieved content doesn't contain information relevant to the query, respond with \"I don't have information about that topic in my knowledge base\" - don't try to provide a general response.\n",[137,49687,49688],{"class":139,"line":3827},[137,49689,49690],{"class":284},"- If the retrieved content only partially addresses the query, only answer what's supported by the content and acknowledge the limitations.\n",[137,49692,49693],{"class":139,"line":3832},[137,49694,49695],{"class":284},"- Use Australian English spelling and grammar.\n",[137,49697,49698],{"class":139,"line":3840},[137,49699,49700],{"class":284},"- Ensure the response is in Markdown format.\n",[137,49702,49703],{"class":139,"line":3846},[137,49704,49705],{"class":284},"- Cite sources inline where applicable using Markdown links to the provided URLs.\n",[137,49707,49708,49711],{"class":139,"line":3861},[137,49709,49710],{"class":284},"- Do not add a separate \"Sources\" section; instead, reference them within the relevant parts of the response.`",[137,49712,3276],{"class":157},[137,49714,49715],{"class":139,"line":3883},[137,49716,516],{"emptyLinePlaceholder":515},[137,49718,49719,49721,49724,49726],{"class":139,"line":3896},[137,49720,3077],{"class":143},[137,49722,49723],{"class":364}," messages",[137,49725,151],{"class":143},[137,49727,49728],{"class":157}," [\n",[137,49730,49731],{"class":139,"line":3901},[137,49732,28051],{"class":157},[137,49734,49735,49738,49741],{"class":139,"line":3906},[137,49736,49737],{"class":157},"        role: ",[137,49739,49740],{"class":284},"\"system\"",[137,49742,1961],{"class":157},[137,49744,49745],{"class":139,"line":3911},[137,49746,49747],{"class":157},"        content: systemMessage,\n",[137,49749,49750],{"class":139,"line":4666},[137,49751,775],{"class":157},[137,49753,49754,49757,49759,49762,49765,49768,49771],{"class":139,"line":4672},[137,49755,49756],{"class":157},"    { role: ",[137,49758,15823],{"class":284},[137,49760,49761],{"class":157},", content: ",[137,49763,49764],{"class":284},"`User query: \"${",[137,49766,49767],{"class":157},"query",[137,49769,49770],{"class":284},"}\"`",[137,49772,31293],{"class":157},[137,49774,49775],{"class":139,"line":4680},[137,49776,5727],{"class":157},[137,49778,49779],{"class":139,"line":4711},[137,49780,516],{"emptyLinePlaceholder":515},[137,49782,49783,49785,49788,49790,49792,49794],{"class":139,"line":4716},[137,49784,3077],{"class":143},[137,49786,49787],{"class":364}," response",[137,49789,151],{"class":143},[137,49791,15069],{"class":143},[137,49793,49435],{"class":147},[137,49795,49796],{"class":157},"(messages, {\n",[137,49798,49799,49802,49805],{"class":139,"line":4721},[137,49800,49801],{"class":157},"    max_new_tokens: ",[137,49803,49804],{"class":364},"4096",[137,49806,1961],{"class":157},[137,49808,49809,49812,49814],{"class":139,"line":4727},[137,49810,49811],{"class":157},"    temperature: ",[137,49813,47589],{"class":364},[137,49815,1961],{"class":157},[137,49817,49818],{"class":139,"line":4732},[137,49819,5422],{"class":157},[27,49821,49822],{},"This allows the model to generate responses that are both accurate and context-aware.",[104,49824,49826],{"id":49825},"putting-it-all-together","Putting It All Together",[27,49828,49829],{},"Now that we have our core functionality defined, let's assemble everything.",[27,49831,49832],{},"We need a UI for users to enter search queries and a backend API that streams responses as discussed above.",[123,49834,49836],{"id":49835},"backend-api-logic","Backend API Logic",[27,49838,49839,49840,49843,49844,49847,49848,1017],{},"Let's start with the backend API. Next.js provides built-in API support—we just need to create an ",[22,49841,49842],{},"\u002Fapi"," directory inside the existing ",[22,49845,49846],{},"\u002Fapp"," directory, then create ",[22,49849,49850],{},"\u002Fsearch\u002Froute.ts",[27,49852,49853],{},"To implement streaming responses, we'll define the stream in the response as follows:",[27,49855,23191,49856,49859,49860,49863],{},[22,49857,49858],{},"\u002Fapi\u002Fsearch\u002Froute.ts",", we'll modify our previous ",[22,49861,49862],{},"textGenerator()"," function to handle streaming:",[128,49865,49867],{"className":13299,"code":49866,"language":13301,"meta":133,"style":133},"const textGenerator = await pipeline(\"text-generation\", \".\u002Flocal_models\u002FPhi-3-mini-4k-instruct\u002F\", {\n    local_files_only: true,\n});\n\n\u002F\u002F ... Existing Code\n\n\u002F\u002F Set up streaming response\nconst encoder = new TextEncoder();\nconst stream = new TransformStream();\nconst writer = stream.writable.getWriter();\n\n\u002F\u002F Create streamer with callback function to stream chunks as they're generated\nconst streamer = new TextStreamer(textGenerator.tokenizer, {\n    skip_prompt: true,\n    callback_function: async (text) => {\n        \u002F\u002F Send the text chunk to the client\n        await writer.write(encoder.encode(`data: ${JSON.stringify({ text })}\\n\\n`));\n    },\n});\n\n\u002F\u002F Start the generation process without awaiting its completion\ntextGenerator(messages, {\n    max_new_tokens: 4096,\n    temperature: 0.2,\n    streamer,\n})\n    .then(async () => {\n        \u002F\u002F When generation is complete, close the stream\n        await writer.write(encoder.encode(\"data: [DONE]\\n\\n\"));\n        await writer.close();\n    })\n    .catch(async (error) => {\n        \u002F\u002F Handle errors during generation\n        console.error(\"Generation error:\", error);\n        await writer.write(encoder.encode(`data: ${JSON.stringify({ error: \"Generation failed\" })}\\n\\n`));\n        await writer.close();\n    });\n\n\u002F\u002F Return a streaming response\nreturn new Response(stream.readable, {\n    headers: {\n        \"Content-Type\": \"text\u002Fevent-stream\",\n        \"Cache-Control\": \"no-cache, no-transform\",\n        Connection: \"keep-alive\",\n    },\n});\n",[22,49868,49869,49891,49899,49903,49907,49912,49916,49921,49937,49953,49970,49974,49979,49996,50005,50024,50029,50071,50075,50079,50083,50088,50095,50103,50111,50116,50120,50136,50141,50164,50174,50178,50198,50203,50216,50254,50264,50268,50272,50277,50289,50294,50306,50318,50328,50332],{"__ignoreMap":133},[137,49870,49871,49873,49875,49877,49879,49881,49883,49885,49887,49889],{"class":139,"line":140},[137,49872,3077],{"class":143},[137,49874,49435],{"class":364},[137,49876,151],{"class":143},[137,49878,15069],{"class":143},[137,49880,46334],{"class":147},[137,49882,356],{"class":157},[137,49884,49446],{"class":284},[137,49886,164],{"class":157},[137,49888,49451],{"class":284},[137,49890,5396],{"class":157},[137,49892,49893,49895,49897],{"class":139,"line":173},[137,49894,49261],{"class":157},[137,49896,3097],{"class":364},[137,49898,1961],{"class":157},[137,49900,49901],{"class":139,"line":188},[137,49902,5422],{"class":157},[137,49904,49905],{"class":139,"line":269},[137,49906,516],{"emptyLinePlaceholder":515},[137,49908,49909],{"class":139,"line":278},[137,49910,49911],{"class":308},"\u002F\u002F ... Existing Code\n",[137,49913,49914],{"class":139,"line":291},[137,49915,516],{"emptyLinePlaceholder":515},[137,49917,49918],{"class":139,"line":297},[137,49919,49920],{"class":308},"\u002F\u002F Set up streaming response\n",[137,49922,49923,49925,49928,49930,49932,49935],{"class":139,"line":302},[137,49924,3077],{"class":143},[137,49926,49927],{"class":364}," encoder",[137,49929,151],{"class":143},[137,49931,1426],{"class":143},[137,49933,49934],{"class":147}," TextEncoder",[137,49936,924],{"class":157},[137,49938,49939,49941,49944,49946,49948,49951],{"class":139,"line":662},[137,49940,3077],{"class":143},[137,49942,49943],{"class":364}," stream",[137,49945,151],{"class":143},[137,49947,1426],{"class":143},[137,49949,49950],{"class":147}," TransformStream",[137,49952,924],{"class":157},[137,49954,49955,49957,49960,49962,49965,49968],{"class":139,"line":667},[137,49956,3077],{"class":143},[137,49958,49959],{"class":364}," writer",[137,49961,151],{"class":143},[137,49963,49964],{"class":157}," stream.writable.",[137,49966,49967],{"class":147},"getWriter",[137,49969,924],{"class":157},[137,49971,49972],{"class":139,"line":786},[137,49973,516],{"emptyLinePlaceholder":515},[137,49975,49976],{"class":139,"line":798},[137,49977,49978],{"class":308},"\u002F\u002F Create streamer with callback function to stream chunks as they're generated\n",[137,49980,49981,49983,49986,49988,49990,49993],{"class":139,"line":803},[137,49982,3077],{"class":143},[137,49984,49985],{"class":364}," streamer",[137,49987,151],{"class":143},[137,49989,1426],{"class":143},[137,49991,49992],{"class":147}," TextStreamer",[137,49994,49995],{"class":157},"(textGenerator.tokenizer, {\n",[137,49997,49998,50001,50003],{"class":139,"line":931},[137,49999,50000],{"class":157},"    skip_prompt: ",[137,50002,3097],{"class":364},[137,50004,1961],{"class":157},[137,50006,50007,50010,50012,50014,50016,50018,50020,50022],{"class":139,"line":1568},[137,50008,50009],{"class":147},"    callback_function",[137,50011,726],{"class":157},[137,50013,15050],{"class":143},[137,50015,158],{"class":157},[137,50017,5189],{"class":161},[137,50019,219],{"class":157},[137,50021,222],{"class":143},[137,50023,256],{"class":157},[137,50025,50026],{"class":139,"line":1573},[137,50027,50028],{"class":308},"        \u002F\u002F Send the text chunk to the client\n",[137,50030,50031,50033,50036,50038,50041,50044,50046,50049,50051,50053,50055,50057,50059,50062,50064,50067,50069],{"class":139,"line":1578},[137,50032,25043],{"class":143},[137,50034,50035],{"class":157}," writer.",[137,50037,3335],{"class":147},[137,50039,50040],{"class":157},"(encoder.",[137,50042,50043],{"class":147},"encode",[137,50045,356],{"class":157},[137,50047,50048],{"class":284},"`data: ${",[137,50050,22554],{"class":364},[137,50052,1017],{"class":284},[137,50054,24816],{"class":147},[137,50056,36896],{"class":284},[137,50058,5189],{"class":157},[137,50060,50061],{"class":284}," })",[137,50063,30476],{"class":284},[137,50065,50066],{"class":364},"\\n\\n",[137,50068,22056],{"class":284},[137,50070,8614],{"class":157},[137,50072,50073],{"class":139,"line":1588},[137,50074,775],{"class":157},[137,50076,50077],{"class":139,"line":1601},[137,50078,5422],{"class":157},[137,50080,50081],{"class":139,"line":3802},[137,50082,516],{"emptyLinePlaceholder":515},[137,50084,50085],{"class":139,"line":3808},[137,50086,50087],{"class":308},"\u002F\u002F Start the generation process without awaiting its completion\n",[137,50089,50090,50093],{"class":139,"line":3822},[137,50091,50092],{"class":147},"textGenerator",[137,50094,49796],{"class":157},[137,50096,50097,50099,50101],{"class":139,"line":3827},[137,50098,49801],{"class":157},[137,50100,49804],{"class":364},[137,50102,1961],{"class":157},[137,50104,50105,50107,50109],{"class":139,"line":3832},[137,50106,49811],{"class":157},[137,50108,47589],{"class":364},[137,50110,1961],{"class":157},[137,50112,50113],{"class":139,"line":3840},[137,50114,50115],{"class":157},"    streamer,\n",[137,50117,50118],{"class":139,"line":3846},[137,50119,13451],{"class":157},[137,50121,50122,50124,50126,50128,50130,50132,50134],{"class":139,"line":3861},[137,50123,2748],{"class":157},[137,50125,2771],{"class":147},[137,50127,356],{"class":157},[137,50129,15050],{"class":143},[137,50131,1484],{"class":157},[137,50133,222],{"class":143},[137,50135,256],{"class":157},[137,50137,50138],{"class":139,"line":3883},[137,50139,50140],{"class":308},"        \u002F\u002F When generation is complete, close the stream\n",[137,50142,50143,50145,50147,50149,50151,50153,50155,50158,50160,50162],{"class":139,"line":3896},[137,50144,25043],{"class":143},[137,50146,50035],{"class":157},[137,50148,3335],{"class":147},[137,50150,50040],{"class":157},[137,50152,50043],{"class":147},[137,50154,356],{"class":157},[137,50156,50157],{"class":284},"\"data: [DONE]",[137,50159,50066],{"class":364},[137,50161,28792],{"class":284},[137,50163,8614],{"class":157},[137,50165,50166,50168,50170,50172],{"class":139,"line":3901},[137,50167,25043],{"class":143},[137,50169,50035],{"class":157},[137,50171,31745],{"class":147},[137,50173,924],{"class":157},[137,50175,50176],{"class":139,"line":3906},[137,50177,2800],{"class":157},[137,50179,50180,50182,50184,50186,50188,50190,50192,50194,50196],{"class":139,"line":3911},[137,50181,2748],{"class":157},[137,50183,2807],{"class":147},[137,50185,356],{"class":157},[137,50187,15050],{"class":143},[137,50189,158],{"class":157},[137,50191,2812],{"class":161},[137,50193,219],{"class":157},[137,50195,222],{"class":143},[137,50197,256],{"class":157},[137,50199,50200],{"class":139,"line":4666},[137,50201,50202],{"class":308},"        \u002F\u002F Handle errors during generation\n",[137,50204,50205,50207,50209,50211,50214],{"class":139,"line":4672},[137,50206,350],{"class":157},[137,50208,2812],{"class":147},[137,50210,356],{"class":157},[137,50212,50213],{"class":284},"\"Generation error:\"",[137,50215,17836],{"class":157},[137,50217,50218,50220,50222,50224,50226,50228,50230,50232,50234,50236,50238,50241,50244,50246,50248,50250,50252],{"class":139,"line":4680},[137,50219,25043],{"class":143},[137,50221,50035],{"class":157},[137,50223,3335],{"class":147},[137,50225,50040],{"class":157},[137,50227,50043],{"class":147},[137,50229,356],{"class":157},[137,50231,50048],{"class":284},[137,50233,22554],{"class":364},[137,50235,1017],{"class":284},[137,50237,24816],{"class":147},[137,50239,50240],{"class":284},"({ error: ",[137,50242,50243],{"class":284},"\"Generation failed\"",[137,50245,50061],{"class":284},[137,50247,30476],{"class":284},[137,50249,50066],{"class":364},[137,50251,22056],{"class":284},[137,50253,8614],{"class":157},[137,50255,50256,50258,50260,50262],{"class":139,"line":4711},[137,50257,25043],{"class":143},[137,50259,50035],{"class":157},[137,50261,31745],{"class":147},[137,50263,924],{"class":157},[137,50265,50266],{"class":139,"line":4716},[137,50267,2832],{"class":157},[137,50269,50270],{"class":139,"line":4721},[137,50271,516],{"emptyLinePlaceholder":515},[137,50273,50274],{"class":139,"line":4727},[137,50275,50276],{"class":308},"\u002F\u002F Return a streaming response\n",[137,50278,50279,50281,50283,50286],{"class":139,"line":4732},[137,50280,5428],{"class":143},[137,50282,1426],{"class":143},[137,50284,50285],{"class":147}," Response",[137,50287,50288],{"class":157},"(stream.readable, {\n",[137,50290,50291],{"class":139,"line":5006},[137,50292,50293],{"class":157},"    headers: {\n",[137,50295,50296,50299,50301,50304],{"class":139,"line":5014},[137,50297,50298],{"class":284},"        \"Content-Type\"",[137,50300,726],{"class":157},[137,50302,50303],{"class":284},"\"text\u002Fevent-stream\"",[137,50305,1961],{"class":157},[137,50307,50308,50311,50313,50316],{"class":139,"line":14343},[137,50309,50310],{"class":284},"        \"Cache-Control\"",[137,50312,726],{"class":157},[137,50314,50315],{"class":284},"\"no-cache, no-transform\"",[137,50317,1961],{"class":157},[137,50319,50320,50323,50326],{"class":139,"line":24199},[137,50321,50322],{"class":157},"        Connection: ",[137,50324,50325],{"class":284},"\"keep-alive\"",[137,50327,1961],{"class":157},[137,50329,50330],{"class":139,"line":24773},[137,50331,775],{"class":157},[137,50333,50334],{"class":139,"line":24778},[137,50335,5422],{"class":157},[27,50337,50338,50339,50342,50343,50346,50347,50350,50351,50354,50355,50358,50359,50362],{},"We initialize the text generation pipeline and create a ",[22,50340,50341],{},"TransformStream"," to handle streaming data. A ",[22,50344,50345],{},"TextStreamer"," with a callback function streams generated text chunks to the client in real-time using a ",[22,50348,50349],{},"TextEncoder",". The text generation process runs asynchronously, sending each chunk to the client as ",[22,50352,50353],{},"data: { text }",". When generation completes, a ",[22,50356,50357],{},"[DONE]"," marker is sent and the stream closes. The response returns as ",[22,50360,50361],{},"text\u002Fevent-stream",", enabling real-time data reception.",[123,50364,50366],{"id":50365},"frontend-logic","Frontend Logic",[27,50368,50369],{},"For our AI search engine's frontend, we'll create a UI that handles real-time streaming of search results for a smooth user experience.",[27,50371,50372,50373,50376],{},"We'll implement this in ",[22,50374,50375],{},"src\u002Fapp\u002Fpage.tsx",", setting up the frontend logic with React state management and event source handling.",[128,50378,50381],{"className":50379,"code":50380,"language":29200,"meta":133,"style":133},"language-tsx shiki shiki-themes github-light github-dark","const [result, setResult] = useState(\"\");\nconst [loading, setLoading] = useState(false);\nconst [error, setError] = useState(\"\");\nconst [rawResults, setRawResults] = useState\u003CArray\u003C{ content: string; articlePath: string }>>([]);\n\ntry {\n    const eventSource = new EventSource(`\u002Fapi\u002Fsearch?query=${encodeURIComponent(query)}`);\n\n    eventSource.onmessage = (event) => {\n        if (event.data === \"[DONE]\") {\n            eventSource.close();\n            setLoading(false);\n            return;\n        }\n\n        try {\n            const data = JSON.parse(event.data);\n            if (data.error) {\n                setError(data.error);\n                setLoading(false);\n                eventSource.close();\n            } else {\n                setResult((prev) => prev + (data.text || \"\"));\n        } catch (err) {\n            console.error(\"Error parsing event data:\", err);\n        }\n    };\n\n    eventSource.onerror = () => {\n        setError(\"An error occurred while fetching results. Please try again.\");\n        setLoading(false);\n        eventSource.close();\n    };\n} catch (err) {\n    console.error(\"Error connecting to search service:\", err);\n    setError(\"Failed to connect to search service.\");\n    setLoading(false);\n}\n",[22,50382,50383,50408,50434,50459,50502,50506,50513,50545,50549,50568,50581,50590,50601,50607,50611,50615,50621,50638,50645,50653,50664,50673,50682,50711,50720,50734,50738,50742,50746,50761,50773,50784,50793,50797,50806,50819,50831,50842],{"__ignoreMap":133},[137,50384,50385,50387,50389,50391,50393,50396,50398,50400,50402,50404,50406],{"class":139,"line":140},[137,50386,3077],{"class":143},[137,50388,22130],{"class":157},[137,50390,27373],{"class":364},[137,50392,164],{"class":157},[137,50394,50395],{"class":364},"setResult",[137,50397,5796],{"class":157},[137,50399,253],{"class":143},[137,50401,37266],{"class":147},[137,50403,356],{"class":157},[137,50405,4535],{"class":284},[137,50407,1502],{"class":157},[137,50409,50410,50412,50414,50417,50419,50422,50424,50426,50428,50430,50432],{"class":139,"line":173},[137,50411,3077],{"class":143},[137,50413,22130],{"class":157},[137,50415,50416],{"class":364},"loading",[137,50418,164],{"class":157},[137,50420,50421],{"class":364},"setLoading",[137,50423,5796],{"class":157},[137,50425,253],{"class":143},[137,50427,37266],{"class":147},[137,50429,356],{"class":157},[137,50431,30105],{"class":364},[137,50433,1502],{"class":157},[137,50435,50436,50438,50440,50442,50444,50447,50449,50451,50453,50455,50457],{"class":139,"line":188},[137,50437,3077],{"class":143},[137,50439,22130],{"class":157},[137,50441,2812],{"class":364},[137,50443,164],{"class":157},[137,50445,50446],{"class":364},"setError",[137,50448,5796],{"class":157},[137,50450,253],{"class":143},[137,50452,37266],{"class":147},[137,50454,356],{"class":157},[137,50456,4535],{"class":284},[137,50458,1502],{"class":157},[137,50460,50461,50463,50465,50468,50470,50473,50475,50477,50479,50481,50483,50485,50487,50489,50491,50493,50495,50497,50499],{"class":139,"line":269},[137,50462,3077],{"class":143},[137,50464,22130],{"class":157},[137,50466,50467],{"class":364},"rawResults",[137,50469,164],{"class":157},[137,50471,50472],{"class":364},"setRawResults",[137,50474,5796],{"class":157},[137,50476,253],{"class":143},[137,50478,37266],{"class":147},[137,50480,4033],{"class":157},[137,50482,45058],{"class":147},[137,50484,35861],{"class":157},[137,50486,29728],{"class":161},[137,50488,894],{"class":143},[137,50490,13630],{"class":364},[137,50492,2323],{"class":157},[137,50494,49615],{"class":161},[137,50496,894],{"class":143},[137,50498,13630],{"class":364},[137,50500,50501],{"class":157}," }>>([]);\n",[137,50503,50504],{"class":139,"line":278},[137,50505,516],{"emptyLinePlaceholder":515},[137,50507,50508,50511],{"class":139,"line":291},[137,50509,50510],{"class":143},"try",[137,50512,256],{"class":157},[137,50514,50515,50517,50520,50522,50524,50527,50529,50532,50535,50537,50539,50541,50543],{"class":139,"line":297},[137,50516,4177],{"class":143},[137,50518,50519],{"class":364}," eventSource",[137,50521,151],{"class":143},[137,50523,1426],{"class":143},[137,50525,50526],{"class":147}," EventSource",[137,50528,356],{"class":157},[137,50530,50531],{"class":284},"`\u002Fapi\u002Fsearch?query=${",[137,50533,50534],{"class":147},"encodeURIComponent",[137,50536,356],{"class":284},[137,50538,49767],{"class":157},[137,50540,14105],{"class":284},[137,50542,4706],{"class":284},[137,50544,1502],{"class":157},[137,50546,50547],{"class":139,"line":302},[137,50548,516],{"emptyLinePlaceholder":515},[137,50550,50551,50554,50556,50558,50560,50562,50564,50566],{"class":139,"line":662},[137,50552,50553],{"class":157},"    eventSource.",[137,50555,24680],{"class":147},[137,50557,151],{"class":143},[137,50559,158],{"class":157},[137,50561,24689],{"class":161},[137,50563,219],{"class":157},[137,50565,222],{"class":143},[137,50567,256],{"class":157},[137,50569,50570,50572,50574,50576,50579],{"class":139,"line":667},[137,50571,5496],{"class":143},[137,50573,24724],{"class":157},[137,50575,5502],{"class":143},[137,50577,50578],{"class":284}," \"[DONE]\"",[137,50580,170],{"class":157},[137,50582,50583,50586,50588],{"class":139,"line":786},[137,50584,50585],{"class":157},"            eventSource.",[137,50587,31745],{"class":147},[137,50589,924],{"class":157},[137,50591,50592,50595,50597,50599],{"class":139,"line":798},[137,50593,50594],{"class":147},"            setLoading",[137,50596,356],{"class":157},[137,50598,30105],{"class":364},[137,50600,1502],{"class":157},[137,50602,50603,50605],{"class":139,"line":803},[137,50604,4683],{"class":143},[137,50606,3276],{"class":157},[137,50608,50609],{"class":139,"line":931},[137,50610,1966],{"class":157},[137,50612,50613],{"class":139,"line":1568},[137,50614,516],{"emptyLinePlaceholder":515},[137,50616,50617,50619],{"class":139,"line":1573},[137,50618,15697],{"class":143},[137,50620,256],{"class":157},[137,50622,50623,50625,50627,50629,50631,50633,50635],{"class":139,"line":1578},[137,50624,5772],{"class":143},[137,50626,38067],{"class":364},[137,50628,151],{"class":143},[137,50630,17436],{"class":364},[137,50632,1017],{"class":157},[137,50634,17441],{"class":147},[137,50636,50637],{"class":157},"(event.data);\n",[137,50639,50640,50642],{"class":139,"line":1588},[137,50641,5747],{"class":143},[137,50643,50644],{"class":157}," (data.error) {\n",[137,50646,50647,50650],{"class":139,"line":1601},[137,50648,50649],{"class":147},"                setError",[137,50651,50652],{"class":157},"(data.error);\n",[137,50654,50655,50658,50660,50662],{"class":139,"line":3802},[137,50656,50657],{"class":147},"                setLoading",[137,50659,356],{"class":157},[137,50661,30105],{"class":364},[137,50663,1502],{"class":157},[137,50665,50666,50669,50671],{"class":139,"line":3808},[137,50667,50668],{"class":157},"                eventSource.",[137,50670,31745],{"class":147},[137,50672,924],{"class":157},[137,50674,50675,50678,50680],{"class":139,"line":3822},[137,50676,50677],{"class":157},"            } ",[137,50679,24947],{"class":143},[137,50681,256],{"class":157},[137,50683,50684,50687,50689,50692,50694,50696,50699,50701,50704,50707,50709],{"class":139,"line":3827},[137,50685,50686],{"class":147},"                setResult",[137,50688,2774],{"class":157},[137,50690,50691],{"class":161},"prev",[137,50693,219],{"class":157},[137,50695,222],{"class":143},[137,50697,50698],{"class":157}," prev ",[137,50700,182],{"class":143},[137,50702,50703],{"class":157}," (data.text ",[137,50705,50706],{"class":143},"||",[137,50708,4607],{"class":284},[137,50710,8614],{"class":157},[137,50712,50713,50715,50717],{"class":139,"line":3832},[137,50714,15729],{"class":157},[137,50716,2807],{"class":143},[137,50718,50719],{"class":157}," (err) {\n",[137,50721,50722,50724,50726,50728,50731],{"class":139,"line":3840},[137,50723,1493],{"class":157},[137,50725,2812],{"class":147},[137,50727,356],{"class":157},[137,50729,50730],{"class":284},"\"Error parsing event data:\"",[137,50732,50733],{"class":157},", err);\n",[137,50735,50736],{"class":139,"line":3846},[137,50737,1966],{"class":157},[137,50739,50740],{"class":139,"line":3861},[137,50741,1892],{"class":157},[137,50743,50744],{"class":139,"line":3883},[137,50745,516],{"emptyLinePlaceholder":515},[137,50747,50748,50750,50753,50755,50757,50759],{"class":139,"line":3896},[137,50749,50553],{"class":157},[137,50751,50752],{"class":147},"onerror",[137,50754,151],{"class":143},[137,50756,1484],{"class":157},[137,50758,222],{"class":143},[137,50760,256],{"class":157},[137,50762,50763,50766,50768,50771],{"class":139,"line":3901},[137,50764,50765],{"class":147},"        setError",[137,50767,356],{"class":157},[137,50769,50770],{"class":284},"\"An error occurred while fetching results. Please try again.\"",[137,50772,1502],{"class":157},[137,50774,50775,50778,50780,50782],{"class":139,"line":3906},[137,50776,50777],{"class":147},"        setLoading",[137,50779,356],{"class":157},[137,50781,30105],{"class":364},[137,50783,1502],{"class":157},[137,50785,50786,50789,50791],{"class":139,"line":3911},[137,50787,50788],{"class":157},"        eventSource.",[137,50790,31745],{"class":147},[137,50792,924],{"class":157},[137,50794,50795],{"class":139,"line":4666},[137,50796,1892],{"class":157},[137,50798,50799,50802,50804],{"class":139,"line":4672},[137,50800,50801],{"class":157},"} ",[137,50803,2807],{"class":143},[137,50805,50719],{"class":157},[137,50807,50808,50810,50812,50814,50817],{"class":139,"line":4680},[137,50809,493],{"class":157},[137,50811,2812],{"class":147},[137,50813,356],{"class":157},[137,50815,50816],{"class":284},"\"Error connecting to search service:\"",[137,50818,50733],{"class":157},[137,50820,50821,50824,50826,50829],{"class":139,"line":4711},[137,50822,50823],{"class":147},"    setError",[137,50825,356],{"class":157},[137,50827,50828],{"class":284},"\"Failed to connect to search service.\"",[137,50830,1502],{"class":157},[137,50832,50833,50836,50838,50840],{"class":139,"line":4716},[137,50834,50835],{"class":147},"    setLoading",[137,50837,356],{"class":157},[137,50839,30105],{"class":364},[137,50841,1502],{"class":157},[137,50843,50844],{"class":139,"line":4721},[137,50845,510],{"class":157},[27,50847,50848,50849,50852,50853,50856,50857,50859,50860,50862],{},"In this code, we establish a real-time connection to the ",[22,50850,50851],{},"\u002Fapi\u002Fsearch"," endpoint using ",[22,50854,50855],{},"EventSource"," for server-sent events (SSE). The ",[22,50858,24680],{}," handler processes incoming data chunks. When it receives the ",[22,50861,50357],{}," marker, it closes the connection and stops loading. Otherwise, it appends new text to the result state, updating the UI in real-time. This creates a responsive experience where search results appear as they're generated.",[123,50864,50866],{"id":50865},"frontend-ui","Frontend UI",[27,50868,50869,50870,50877],{},"We need a polished UI to display results. We've chosen Tailwind CSS and Markdown rendering for this purpose. The complete project is available on ",[45,50871,50874],{"href":50872,"target":2716,"rel":50873},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fbuilding-an-ai-search-engine-with-nextjs-and-free-open-source-llms",[2718,2719],[42,50875,50876],{},"GitHub",". Since we've covered the core AI search functionality in detail, here's a streamlined list of our UI components:",[2569,50879,50880],{},[1006,50881,50882,50885],{},[42,50883,50884],{},"Search Input",": A text input field for user queries.",[128,50887,50889],{"className":50379,"code":50888,"language":29200,"meta":133,"style":133},"\u003Cinput\n    type=\"text\"\n    value={query}\n    onChange={(e) => setQuery(e.target.value)}\n    onKeyDown={(e) => e.key === \"Enter\" && !loading && query.trim() && handleSearch()}\n    placeholder=\"What would you like to know?\"\n    className=\"flex-1 min-w-0 block w-full px-5 py-4 text-base border-0 focus:outline-none focus:ring-0\"\n\u002F>\n",[22,50890,50891,50897,50906,50916,50938,50985,50995,51005],{"__ignoreMap":133},[137,50892,50893,50895],{"class":139,"line":140},[137,50894,4033],{"class":157},[137,50896,23909],{"class":4036},[137,50898,50899,50902,50904],{"class":139,"line":173},[137,50900,50901],{"class":147},"    type",[137,50903,253],{"class":143},[137,50905,8349],{"class":284},[137,50907,50908,50911,50913],{"class":139,"line":188},[137,50909,50910],{"class":147},"    value",[137,50912,253],{"class":143},[137,50914,50915],{"class":157},"{query}\n",[137,50917,50918,50921,50923,50926,50928,50930,50932,50935],{"class":139,"line":269},[137,50919,50920],{"class":147},"    onChange",[137,50922,253],{"class":143},[137,50924,50925],{"class":157},"{(",[137,50927,24534],{"class":161},[137,50929,219],{"class":157},[137,50931,222],{"class":143},[137,50933,50934],{"class":147}," setQuery",[137,50936,50937],{"class":157},"(e.target.value)}\n",[137,50939,50940,50943,50945,50947,50949,50951,50953,50956,50958,50961,50963,50965,50968,50970,50973,50976,50978,50980,50983],{"class":139,"line":278},[137,50941,50942],{"class":147},"    onKeyDown",[137,50944,253],{"class":143},[137,50946,50925],{"class":157},[137,50948,24534],{"class":161},[137,50950,219],{"class":157},[137,50952,222],{"class":143},[137,50954,50955],{"class":157}," e.key ",[137,50957,5502],{"class":143},[137,50959,50960],{"class":284}," \"Enter\"",[137,50962,35905],{"class":143},[137,50964,27133],{"class":143},[137,50966,50967],{"class":157},"loading ",[137,50969,3351],{"class":143},[137,50971,50972],{"class":157}," query.",[137,50974,50975],{"class":147},"trim",[137,50977,3348],{"class":157},[137,50979,3351],{"class":143},[137,50981,50982],{"class":147}," handleSearch",[137,50984,30297],{"class":157},[137,50986,50987,50990,50992],{"class":139,"line":291},[137,50988,50989],{"class":147},"    placeholder",[137,50991,253],{"class":143},[137,50993,50994],{"class":284},"\"What would you like to know?\"\n",[137,50996,50997,51000,51002],{"class":139,"line":297},[137,50998,50999],{"class":147},"    className",[137,51001,253],{"class":143},[137,51003,51004],{"class":284},"\"flex-1 min-w-0 block w-full px-5 py-4 text-base border-0 focus:outline-none focus:ring-0\"\n",[137,51006,51007],{"class":139,"line":302},[137,51008,21775],{"class":157},[2569,51010,51011],{"start":173},[1006,51012,51013,51016],{},[42,51014,51015],{},"Search Button",": A button with loading indicator for the search process.",[128,51018,51020],{"className":50379,"code":51019,"language":29200,"meta":133,"style":133},"\u003Cbutton\n    onClick={handleSearch}\n    disabled={loading || !query.trim()}\n    className={`cursor-pointer px-6 py-4 border-0 text-base font-medium text-white ${\n        loading || !query.trim()\n            ? \"bg-indigo-300 cursor-not-allowed\"\n            : \"bg-indigo-600 hover:bg-indigo-700 transition-colors duration-150\"\n    }`}\n>\n    {loading ? (\n        \u003Cdiv className=\"flex items-center\">\n            \u003Csvg\n                className=\"animate-spin -ml-1 mr-2 h-5 w-5 text-white\"\n                xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\"\n                fill=\"none\"\n                viewBox=\"0 0 24 24\"\n            >\n                \u003Ccircle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\">\u003C\u002Fcircle>\n                \u003Cpath\n                    className=\"opacity-75\"\n                    fill=\"currentColor\"\n                    d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n                >\u003C\u002Fpath>\n            \u003C\u002Fsvg>\n            Searching...\n        \u003C\u002Fdiv>\n    ) : (\n        \"Search\"\n    )}\n\u003C\u002Fbutton>\n",[22,51021,51022,51028,51038,51059,51070,51087,51095,51103,51110,51114,51123,51138,51145,51155,51165,51175,51185,51189,51247,51254,51264,51274,51284,51294,51303,51308,51316,51325,51330,51335],{"__ignoreMap":133},[137,51023,51024,51026],{"class":139,"line":140},[137,51025,4033],{"class":157},[137,51027,8385],{"class":4036},[137,51029,51030,51033,51035],{"class":139,"line":173},[137,51031,51032],{"class":147},"    onClick",[137,51034,253],{"class":143},[137,51036,51037],{"class":157},"{handleSearch}\n",[137,51039,51040,51043,51045,51048,51050,51052,51055,51057],{"class":139,"line":188},[137,51041,51042],{"class":147},"    disabled",[137,51044,253],{"class":143},[137,51046,51047],{"class":157},"{loading ",[137,51049,50706],{"class":143},[137,51051,27133],{"class":143},[137,51053,51054],{"class":157},"query.",[137,51056,50975],{"class":147},[137,51058,30297],{"class":157},[137,51060,51061,51063,51065,51067],{"class":139,"line":269},[137,51062,50999],{"class":147},[137,51064,253],{"class":143},[137,51066,30449],{"class":157},[137,51068,51069],{"class":284},"`cursor-pointer px-6 py-4 border-0 text-base font-medium text-white ${\n",[137,51071,51072,51075,51077,51079,51081,51083,51085],{"class":139,"line":278},[137,51073,51074],{"class":157},"        loading",[137,51076,24707],{"class":143},[137,51078,27133],{"class":143},[137,51080,49767],{"class":157},[137,51082,1017],{"class":284},[137,51084,50975],{"class":147},[137,51086,2754],{"class":284},[137,51088,51089,51092],{"class":139,"line":291},[137,51090,51091],{"class":143},"            ?",[137,51093,51094],{"class":284}," \"bg-indigo-300 cursor-not-allowed\"\n",[137,51096,51097,51100],{"class":139,"line":297},[137,51098,51099],{"class":143},"            :",[137,51101,51102],{"class":284}," \"bg-indigo-600 hover:bg-indigo-700 transition-colors duration-150\"\n",[137,51104,51105,51108],{"class":139,"line":302},[137,51106,51107],{"class":284},"    }`",[137,51109,510],{"class":157},[137,51111,51112],{"class":139,"line":662},[137,51113,4053],{"class":157},[137,51115,51116,51119,51121],{"class":139,"line":667},[137,51117,51118],{"class":157},"    {loading ",[137,51120,12972],{"class":143},[137,51122,30009],{"class":157},[137,51124,51125,51127,51129,51131,51133,51136],{"class":139,"line":786},[137,51126,9826],{"class":157},[137,51128,8330],{"class":4036},[137,51130,36916],{"class":147},[137,51132,253],{"class":143},[137,51134,51135],{"class":284},"\"flex items-center\"",[137,51137,4053],{"class":157},[137,51139,51140,51142],{"class":139,"line":798},[137,51141,23852],{"class":157},[137,51143,51144],{"class":4036},"svg\n",[137,51146,51147,51150,51152],{"class":139,"line":803},[137,51148,51149],{"class":147},"                className",[137,51151,253],{"class":143},[137,51153,51154],{"class":284},"\"animate-spin -ml-1 mr-2 h-5 w-5 text-white\"\n",[137,51156,51157,51160,51162],{"class":139,"line":931},[137,51158,51159],{"class":147},"                xmlns",[137,51161,253],{"class":143},[137,51163,51164],{"class":284},"\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\"\n",[137,51166,51167,51170,51172],{"class":139,"line":1568},[137,51168,51169],{"class":147},"                fill",[137,51171,253],{"class":143},[137,51173,51174],{"class":284},"\"none\"\n",[137,51176,51177,51180,51182],{"class":139,"line":1573},[137,51178,51179],{"class":147},"                viewBox",[137,51181,253],{"class":143},[137,51183,51184],{"class":284},"\"0 0 24 24\"\n",[137,51186,51187],{"class":139,"line":1578},[137,51188,25929],{"class":157},[137,51190,51191,51193,51196,51198,51200,51203,51206,51208,51211,51214,51216,51218,51220,51222,51225,51228,51230,51233,51236,51238,51241,51243,51245],{"class":139,"line":1588},[137,51192,23861],{"class":157},[137,51194,51195],{"class":4036},"circle",[137,51197,36916],{"class":147},[137,51199,253],{"class":143},[137,51201,51202],{"class":284},"\"opacity-25\"",[137,51204,51205],{"class":147}," cx",[137,51207,253],{"class":143},[137,51209,51210],{"class":284},"\"12\"",[137,51212,51213],{"class":147}," cy",[137,51215,253],{"class":143},[137,51217,51210],{"class":284},[137,51219,33949],{"class":147},[137,51221,253],{"class":143},[137,51223,51224],{"class":284},"\"10\"",[137,51226,51227],{"class":147}," stroke",[137,51229,253],{"class":143},[137,51231,51232],{"class":284},"\"currentColor\"",[137,51234,51235],{"class":147}," strokeWidth",[137,51237,253],{"class":143},[137,51239,51240],{"class":284},"\"4\"",[137,51242,4048],{"class":157},[137,51244,51195],{"class":4036},[137,51246,4053],{"class":157},[137,51248,51249,51251],{"class":139,"line":1601},[137,51250,23861],{"class":157},[137,51252,51253],{"class":4036},"path\n",[137,51255,51256,51259,51261],{"class":139,"line":3802},[137,51257,51258],{"class":147},"                    className",[137,51260,253],{"class":143},[137,51262,51263],{"class":284},"\"opacity-75\"\n",[137,51265,51266,51269,51271],{"class":139,"line":3808},[137,51267,51268],{"class":147},"                    fill",[137,51270,253],{"class":143},[137,51272,51273],{"class":284},"\"currentColor\"\n",[137,51275,51276,51279,51281],{"class":139,"line":3822},[137,51277,51278],{"class":147},"                    d",[137,51280,253],{"class":143},[137,51282,51283],{"class":284},"\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n",[137,51285,51286,51289,51292],{"class":139,"line":3827},[137,51287,51288],{"class":157},"                >\u003C\u002F",[137,51290,51291],{"class":4036},"path",[137,51293,4053],{"class":157},[137,51295,51296,51298,51301],{"class":139,"line":3832},[137,51297,23980],{"class":157},[137,51299,51300],{"class":4036},"svg",[137,51302,4053],{"class":157},[137,51304,51305],{"class":139,"line":3840},[137,51306,51307],{"class":157},"            Searching...\n",[137,51309,51310,51312,51314],{"class":139,"line":3846},[137,51311,9843],{"class":157},[137,51313,8330],{"class":4036},[137,51315,4053],{"class":157},[137,51317,51318,51321,51323],{"class":139,"line":3861},[137,51319,51320],{"class":157},"    ) ",[137,51322,894],{"class":143},[137,51324,30009],{"class":157},[137,51326,51327],{"class":139,"line":3883},[137,51328,51329],{"class":284},"        \"Search\"\n",[137,51331,51332],{"class":139,"line":3896},[137,51333,51334],{"class":157},"    )}\n",[137,51336,51337,51339,51341],{"class":139,"line":3901},[137,51338,4083],{"class":157},[137,51340,8170],{"class":4036},[137,51342,4053],{"class":157},[2569,51344,51345],{"start":188},[1006,51346,51347,51350],{},[42,51348,51349],{},"Error Handling",": A component for displaying search error messages.",[128,51352,51354],{"className":50379,"code":51353,"language":29200,"meta":133,"style":133},"{\n    error && \u003Cdiv className=\"mt-4 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md\">{error}\u003C\u002Fdiv>;\n}\n",[22,51355,51356,51360,51385],{"__ignoreMap":133},[137,51357,51358],{"class":139,"line":140},[137,51359,15971],{"class":157},[137,51361,51362,51365,51367,51369,51371,51373,51375,51378,51381,51383],{"class":139,"line":173},[137,51363,51364],{"class":157},"    error ",[137,51366,3351],{"class":143},[137,51368,29304],{"class":157},[137,51370,8330],{"class":4036},[137,51372,36916],{"class":147},[137,51374,253],{"class":143},[137,51376,51377],{"class":284},"\"mt-4 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md\"",[137,51379,51380],{"class":157},">{error}\u003C\u002F",[137,51382,8330],{"class":4036},[137,51384,29320],{"class":157},[137,51386,51387],{"class":139,"line":188},[137,51388,510],{"class":157},[2569,51390,51391,51397],{"start":269},[1006,51392,51393,51396],{},[42,51394,51395],{},"Search Results",": Displays the AI-generated response and retrieved source articles.",[1006,51398,51399,51402,51403,51406],{},[42,51400,51401],{},"Markdown Rendering",": Uses ",[22,51404,51405],{},"ReactMarkdown"," for rich text formatting of AI responses.",[128,51408,51410],{"className":50379,"code":51409,"language":29200,"meta":133,"style":133},"\u003CReactMarkdown\n    remarkPlugins={[remarkGfm]}\n    rehypePlugins={[rehypeRaw, rehypeSanitize, rehypeHighlight]}\n    components={{\n        h1: (props) => \u003Ch1 className=\"text-2xl font-bold mt-6 mb-4\" {...props} \u002F>,\n        h2: (props) => \u003Ch2 className=\"text-xl font-bold mt-5 mb-3\" {...props} \u002F>,\n        h3: (props) => \u003Ch3 className=\"text-lg font-bold mt-4 mb-2\" {...props} \u002F>,\n        p: (props) => \u003Cp className=\"mb-4\" {...props} \u002F>,\n        ul: (props) => \u003Cul className=\"list-disc pl-5 mb-4\" {...props} \u002F>,\n        ol: (props) => \u003Col className=\"list-decimal pl-5 mb-4\" {...props} \u002F>,\n        li: (props) => \u003Cli className=\"mb-1\" {...props} \u002F>,\n        a: (props) => (\n            \u003Ca className=\"text-blue-600 hover:underline\" target=\"_blank\" rel=\"noopener noreferrer\" {...props} \u002F>\n        ),\n        blockquote: (props) => \u003Cblockquote className=\"border-l-4 border-gray-200 pl-4 py-2 mb-4 italic\" {...props} \u002F>,\n        code: ({ className, children, ...props }) => {\n            const isChildrenArray = Array.isArray(children);\n            if (!isChildrenArray) {\n                return (\n                    \u003Ccode className={className} {...props}>\n                        {children}\n                    \u003C\u002Fcode>\n                );\n            }\n\n            return (\n                \u003Cpre className=\"bg-gray-100 p-4 rounded overflow-x-auto mb-4\">\n                    \u003Ccode className={className} {...props}>\n                        {children}\n                    \u003C\u002Fcode>\n                \u003C\u002Fpre>\n            );\n        },\n        table: (props) => \u003Ctable className=\"min-w-full divide-y divide-gray-200 mb-4\" {...props} \u002F>,\n        thead: (props) => \u003Cthead className=\"bg-gray-50\" {...props} \u002F>,\n        tbody: (props) => \u003Ctbody className=\"divide-y divide-gray-200\" {...props} \u002F>,\n        tr: (props) => \u003Ctr className=\"hover:bg-gray-50\" {...props} \u002F>,\n        th: (props) => (\n            \u003Cth className=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\" {...props} \u002F>\n        ),\n        td: (props) => \u003Ctd className=\"px-6 py-4 whitespace-nowrap text-sm\" {...props} \u002F>,\n    }}\n>\n    {result}\n\u003C\u002FReactMarkdown>\n",[22,51411,51412,51419,51429,51439,51449,51481,51511,51541,51571,51601,51631,51661,51676,51710,51715,51745,51771,51789,51800,51806,51824,51829,51838,51843,51847,51851,51857,51872,51888,51892,51900,51908,51912,51916,51946,51976,52006,52036,52051,52070,52074,52104,52109,52113,52118],{"__ignoreMap":133},[137,51413,51414,51416],{"class":139,"line":140},[137,51415,4033],{"class":157},[137,51417,51418],{"class":364},"ReactMarkdown\n",[137,51420,51421,51424,51426],{"class":139,"line":173},[137,51422,51423],{"class":147},"    remarkPlugins",[137,51425,253],{"class":143},[137,51427,51428],{"class":157},"{[remarkGfm]}\n",[137,51430,51431,51434,51436],{"class":139,"line":188},[137,51432,51433],{"class":147},"    rehypePlugins",[137,51435,253],{"class":143},[137,51437,51438],{"class":157},"{[rehypeRaw, rehypeSanitize, rehypeHighlight]}\n",[137,51440,51441,51444,51446],{"class":139,"line":269},[137,51442,51443],{"class":147},"    components",[137,51445,253],{"class":143},[137,51447,51448],{"class":157},"{{\n",[137,51450,51451,51454,51457,51459,51461,51463,51465,51467,51469,51471,51474,51476,51478],{"class":139,"line":278},[137,51452,51453],{"class":147},"        h1",[137,51455,51456],{"class":157},": (",[137,51458,29277],{"class":161},[137,51460,219],{"class":157},[137,51462,222],{"class":143},[137,51464,29304],{"class":157},[137,51466,17],{"class":4036},[137,51468,36916],{"class":147},[137,51470,253],{"class":143},[137,51472,51473],{"class":284},"\"text-2xl font-bold mt-6 mb-4\"",[137,51475,22084],{"class":157},[137,51477,14408],{"class":143},[137,51479,51480],{"class":157},"props} \u002F>,\n",[137,51482,51483,51486,51488,51490,51492,51494,51496,51498,51500,51502,51505,51507,51509],{"class":139,"line":291},[137,51484,51485],{"class":147},"        h2",[137,51487,51456],{"class":157},[137,51489,29277],{"class":161},[137,51491,219],{"class":157},[137,51493,222],{"class":143},[137,51495,29304],{"class":157},[137,51497,104],{"class":4036},[137,51499,36916],{"class":147},[137,51501,253],{"class":143},[137,51503,51504],{"class":284},"\"text-xl font-bold mt-5 mb-3\"",[137,51506,22084],{"class":157},[137,51508,14408],{"class":143},[137,51510,51480],{"class":157},[137,51512,51513,51516,51518,51520,51522,51524,51526,51528,51530,51532,51535,51537,51539],{"class":139,"line":297},[137,51514,51515],{"class":147},"        h3",[137,51517,51456],{"class":157},[137,51519,29277],{"class":161},[137,51521,219],{"class":157},[137,51523,222],{"class":143},[137,51525,29304],{"class":157},[137,51527,123],{"class":4036},[137,51529,36916],{"class":147},[137,51531,253],{"class":143},[137,51533,51534],{"class":284},"\"text-lg font-bold mt-4 mb-2\"",[137,51536,22084],{"class":157},[137,51538,14408],{"class":143},[137,51540,51480],{"class":157},[137,51542,51543,51546,51548,51550,51552,51554,51556,51558,51560,51562,51565,51567,51569],{"class":139,"line":302},[137,51544,51545],{"class":147},"        p",[137,51547,51456],{"class":157},[137,51549,29277],{"class":161},[137,51551,219],{"class":157},[137,51553,222],{"class":143},[137,51555,29304],{"class":157},[137,51557,27],{"class":4036},[137,51559,36916],{"class":147},[137,51561,253],{"class":143},[137,51563,51564],{"class":284},"\"mb-4\"",[137,51566,22084],{"class":157},[137,51568,14408],{"class":143},[137,51570,51480],{"class":157},[137,51572,51573,51576,51578,51580,51582,51584,51586,51588,51590,51592,51595,51597,51599],{"class":139,"line":662},[137,51574,51575],{"class":147},"        ul",[137,51577,51456],{"class":157},[137,51579,29277],{"class":161},[137,51581,219],{"class":157},[137,51583,222],{"class":143},[137,51585,29304],{"class":157},[137,51587,1003],{"class":4036},[137,51589,36916],{"class":147},[137,51591,253],{"class":143},[137,51593,51594],{"class":284},"\"list-disc pl-5 mb-4\"",[137,51596,22084],{"class":157},[137,51598,14408],{"class":143},[137,51600,51480],{"class":157},[137,51602,51603,51606,51608,51610,51612,51614,51616,51618,51620,51622,51625,51627,51629],{"class":139,"line":667},[137,51604,51605],{"class":147},"        ol",[137,51607,51456],{"class":157},[137,51609,29277],{"class":161},[137,51611,219],{"class":157},[137,51613,222],{"class":143},[137,51615,29304],{"class":157},[137,51617,2569],{"class":4036},[137,51619,36916],{"class":147},[137,51621,253],{"class":143},[137,51623,51624],{"class":284},"\"list-decimal pl-5 mb-4\"",[137,51626,22084],{"class":157},[137,51628,14408],{"class":143},[137,51630,51480],{"class":157},[137,51632,51633,51636,51638,51640,51642,51644,51646,51648,51650,51652,51655,51657,51659],{"class":139,"line":786},[137,51634,51635],{"class":147},"        li",[137,51637,51456],{"class":157},[137,51639,29277],{"class":161},[137,51641,219],{"class":157},[137,51643,222],{"class":143},[137,51645,29304],{"class":157},[137,51647,1006],{"class":4036},[137,51649,36916],{"class":147},[137,51651,253],{"class":143},[137,51653,51654],{"class":284},"\"mb-1\"",[137,51656,22084],{"class":157},[137,51658,14408],{"class":143},[137,51660,51480],{"class":157},[137,51662,51663,51666,51668,51670,51672,51674],{"class":139,"line":798},[137,51664,51665],{"class":147},"        a",[137,51667,51456],{"class":157},[137,51669,29277],{"class":161},[137,51671,219],{"class":157},[137,51673,222],{"class":143},[137,51675,30009],{"class":157},[137,51677,51678,51680,51682,51684,51686,51689,51691,51693,51696,51698,51700,51703,51705,51707],{"class":139,"line":803},[137,51679,23852],{"class":157},[137,51681,45],{"class":4036},[137,51683,36916],{"class":147},[137,51685,253],{"class":143},[137,51687,51688],{"class":284},"\"text-blue-600 hover:underline\"",[137,51690,35527],{"class":147},[137,51692,253],{"class":143},[137,51694,51695],{"class":284},"\"_blank\"",[137,51697,23226],{"class":147},[137,51699,253],{"class":143},[137,51701,51702],{"class":284},"\"noopener noreferrer\"",[137,51704,22084],{"class":157},[137,51706,14408],{"class":143},[137,51708,51709],{"class":157},"props} \u002F>\n",[137,51711,51712],{"class":139,"line":931},[137,51713,51714],{"class":157},"        ),\n",[137,51716,51717,51720,51722,51724,51726,51728,51730,51732,51734,51736,51739,51741,51743],{"class":139,"line":1568},[137,51718,51719],{"class":147},"        blockquote",[137,51721,51456],{"class":157},[137,51723,29277],{"class":161},[137,51725,219],{"class":157},[137,51727,222],{"class":143},[137,51729,29304],{"class":157},[137,51731,3244],{"class":4036},[137,51733,36916],{"class":147},[137,51735,253],{"class":143},[137,51737,51738],{"class":284},"\"border-l-4 border-gray-200 pl-4 py-2 mb-4 italic\"",[137,51740,22084],{"class":157},[137,51742,14408],{"class":143},[137,51744,51480],{"class":157},[137,51746,51747,51750,51753,51755,51757,51759,51761,51763,51765,51767,51769],{"class":139,"line":1573},[137,51748,51749],{"class":147},"        code",[137,51751,51752],{"class":157},": ({ ",[137,51754,39866],{"class":161},[137,51756,164],{"class":157},[137,51758,37244],{"class":161},[137,51760,164],{"class":157},[137,51762,14408],{"class":143},[137,51764,29277],{"class":161},[137,51766,29299],{"class":157},[137,51768,222],{"class":143},[137,51770,256],{"class":157},[137,51772,51773,51775,51778,51780,51783,51786],{"class":139,"line":1578},[137,51774,5772],{"class":143},[137,51776,51777],{"class":364}," isChildrenArray",[137,51779,151],{"class":143},[137,51781,51782],{"class":157}," Array.",[137,51784,51785],{"class":147},"isArray",[137,51787,51788],{"class":157},"(children);\n",[137,51790,51791,51793,51795,51797],{"class":139,"line":1588},[137,51792,5747],{"class":143},[137,51794,158],{"class":157},[137,51796,17393],{"class":143},[137,51798,51799],{"class":157},"isChildrenArray) {\n",[137,51801,51802,51804],{"class":139,"line":1601},[137,51803,5761],{"class":143},[137,51805,30009],{"class":157},[137,51807,51808,51810,51812,51814,51816,51819,51821],{"class":139,"line":3802},[137,51809,23906],{"class":157},[137,51811,22],{"class":4036},[137,51813,36916],{"class":147},[137,51815,253],{"class":143},[137,51817,51818],{"class":157},"{className} {",[137,51820,14408],{"class":143},[137,51822,51823],{"class":157},"props}>\n",[137,51825,51826],{"class":139,"line":3808},[137,51827,51828],{"class":157},"                        {children}\n",[137,51830,51831,51834,51836],{"class":139,"line":3822},[137,51832,51833],{"class":157},"                    \u003C\u002F",[137,51835,22],{"class":4036},[137,51837,4053],{"class":157},[137,51839,51840],{"class":139,"line":3827},[137,51841,51842],{"class":157},"                );\n",[137,51844,51845],{"class":139,"line":3832},[137,51846,760],{"class":157},[137,51848,51849],{"class":139,"line":3840},[137,51850,516],{"emptyLinePlaceholder":515},[137,51852,51853,51855],{"class":139,"line":3846},[137,51854,4683],{"class":143},[137,51856,30009],{"class":157},[137,51858,51859,51861,51863,51865,51867,51870],{"class":139,"line":3861},[137,51860,23861],{"class":157},[137,51862,128],{"class":4036},[137,51864,36916],{"class":147},[137,51866,253],{"class":143},[137,51868,51869],{"class":284},"\"bg-gray-100 p-4 rounded overflow-x-auto mb-4\"",[137,51871,4053],{"class":157},[137,51873,51874,51876,51878,51880,51882,51884,51886],{"class":139,"line":3883},[137,51875,23906],{"class":157},[137,51877,22],{"class":4036},[137,51879,36916],{"class":147},[137,51881,253],{"class":143},[137,51883,51818],{"class":157},[137,51885,14408],{"class":143},[137,51887,51823],{"class":157},[137,51889,51890],{"class":139,"line":3896},[137,51891,51828],{"class":157},[137,51893,51894,51896,51898],{"class":139,"line":3901},[137,51895,51833],{"class":157},[137,51897,22],{"class":4036},[137,51899,4053],{"class":157},[137,51901,51902,51904,51906],{"class":139,"line":3906},[137,51903,23971],{"class":157},[137,51905,128],{"class":4036},[137,51907,4053],{"class":157},[137,51909,51910],{"class":139,"line":3911},[137,51911,6093],{"class":157},[137,51913,51914],{"class":139,"line":4666},[137,51915,2084],{"class":157},[137,51917,51918,51921,51923,51925,51927,51929,51931,51933,51935,51937,51940,51942,51944],{"class":139,"line":4672},[137,51919,51920],{"class":147},"        table",[137,51922,51456],{"class":157},[137,51924,29277],{"class":161},[137,51926,219],{"class":157},[137,51928,222],{"class":143},[137,51930,29304],{"class":157},[137,51932,45740],{"class":4036},[137,51934,36916],{"class":147},[137,51936,253],{"class":143},[137,51938,51939],{"class":284},"\"min-w-full divide-y divide-gray-200 mb-4\"",[137,51941,22084],{"class":157},[137,51943,14408],{"class":143},[137,51945,51480],{"class":157},[137,51947,51948,51951,51953,51955,51957,51959,51961,51963,51965,51967,51970,51972,51974],{"class":139,"line":4680},[137,51949,51950],{"class":147},"        thead",[137,51952,51456],{"class":157},[137,51954,29277],{"class":161},[137,51956,219],{"class":157},[137,51958,222],{"class":143},[137,51960,29304],{"class":157},[137,51962,45743],{"class":4036},[137,51964,36916],{"class":147},[137,51966,253],{"class":143},[137,51968,51969],{"class":284},"\"bg-gray-50\"",[137,51971,22084],{"class":157},[137,51973,14408],{"class":143},[137,51975,51480],{"class":157},[137,51977,51978,51981,51983,51985,51987,51989,51991,51993,51995,51997,52000,52002,52004],{"class":139,"line":4711},[137,51979,51980],{"class":147},"        tbody",[137,51982,51456],{"class":157},[137,51984,29277],{"class":161},[137,51986,219],{"class":157},[137,51988,222],{"class":143},[137,51990,29304],{"class":157},[137,51992,45762],{"class":4036},[137,51994,36916],{"class":147},[137,51996,253],{"class":143},[137,51998,51999],{"class":284},"\"divide-y divide-gray-200\"",[137,52001,22084],{"class":157},[137,52003,14408],{"class":143},[137,52005,51480],{"class":157},[137,52007,52008,52011,52013,52015,52017,52019,52021,52023,52025,52027,52030,52032,52034],{"class":139,"line":4716},[137,52009,52010],{"class":147},"        tr",[137,52012,51456],{"class":157},[137,52014,29277],{"class":161},[137,52016,219],{"class":157},[137,52018,222],{"class":143},[137,52020,29304],{"class":157},[137,52022,45746],{"class":4036},[137,52024,36916],{"class":147},[137,52026,253],{"class":143},[137,52028,52029],{"class":284},"\"hover:bg-gray-50\"",[137,52031,22084],{"class":157},[137,52033,14408],{"class":143},[137,52035,51480],{"class":157},[137,52037,52038,52041,52043,52045,52047,52049],{"class":139,"line":4721},[137,52039,52040],{"class":147},"        th",[137,52042,51456],{"class":157},[137,52044,29277],{"class":161},[137,52046,219],{"class":157},[137,52048,222],{"class":143},[137,52050,30009],{"class":157},[137,52052,52053,52055,52057,52059,52061,52064,52066,52068],{"class":139,"line":4727},[137,52054,23852],{"class":157},[137,52056,45749],{"class":4036},[137,52058,36916],{"class":147},[137,52060,253],{"class":143},[137,52062,52063],{"class":284},"\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\"",[137,52065,22084],{"class":157},[137,52067,14408],{"class":143},[137,52069,51709],{"class":157},[137,52071,52072],{"class":139,"line":4732},[137,52073,51714],{"class":157},[137,52075,52076,52079,52081,52083,52085,52087,52089,52091,52093,52095,52098,52100,52102],{"class":139,"line":5006},[137,52077,52078],{"class":147},"        td",[137,52080,51456],{"class":157},[137,52082,29277],{"class":161},[137,52084,219],{"class":157},[137,52086,222],{"class":143},[137,52088,29304],{"class":157},[137,52090,45767],{"class":4036},[137,52092,36916],{"class":147},[137,52094,253],{"class":143},[137,52096,52097],{"class":284},"\"px-6 py-4 whitespace-nowrap text-sm\"",[137,52099,22084],{"class":157},[137,52101,14408],{"class":143},[137,52103,51480],{"class":157},[137,52105,52106],{"class":139,"line":5014},[137,52107,52108],{"class":157},"    }}\n",[137,52110,52111],{"class":139,"line":14343},[137,52112,4053],{"class":157},[137,52114,52115],{"class":139,"line":24199},[137,52116,52117],{"class":157},"    {result}\n",[137,52119,52120,52122,52124],{"class":139,"line":24773},[137,52121,4083],{"class":157},[137,52123,51405],{"class":364},[137,52125,4053],{"class":157},[104,52127,52129],{"id":52128},"deployment","Deployment",[27,52131,52132,52133,52136],{},"For deployment, we'll need a server with a Node.js environment. Serverless platforms like Vercel or Netlify won't support this application because the ",[22,52134,52135],{},"better-sqlite3"," package requires filesystem access for database storage, which isn't available in serverless environments. Also note that local model files are quite large—platforms like Vercel's free tier only support up to 100MB per static file.",[27,52138,52139],{},"Additionally, since these models run on a server, we must ensure sufficient computational resources to prevent crashes.",[2617,52141,52142],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":133,"searchDepth":173,"depth":173,"links":52144},[52145,52148,52149,52150,52151,52155,52158,52159,52160,52165],{"id":47758,"depth":173,"text":47759,"children":52146},[52147],{"id":47784,"depth":188,"text":47785},{"id":47813,"depth":173,"text":47814},{"id":47843,"depth":173,"text":47844},{"id":47871,"depth":173,"text":47872},{"id":47891,"depth":173,"text":47892,"children":52152},[52153,52154],{"id":47920,"depth":188,"text":47921},{"id":48005,"depth":188,"text":48006},{"id":48382,"depth":173,"text":48383,"children":52156},[52157],{"id":48399,"depth":188,"text":48400},{"id":49196,"depth":173,"text":49197},{"id":49416,"depth":173,"text":49417},{"id":49825,"depth":173,"text":49826,"children":52161},[52162,52163,52164],{"id":49835,"depth":188,"text":49836},{"id":50365,"depth":188,"text":50366},{"id":50865,"depth":188,"text":50866},{"id":52128,"depth":173,"text":52129},"Build your own AI search engine with Next.js, SQLite, and free open-source LLMs. This tutorial guides you through implementing Retrieval-Augmented Generation (RAG) for accurate, context-aware search, bypassing costly APIs. Discover how to leverage local models and vector databases for powerful, privacy-focused search results.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1743826108\u002Fblog\u002Fbuilding-an-ai-search-engine-with-nextjs-and-free-open-source-llms\u002FChatGPT_Image_Apr_5_2025_01_58_48_PM_famrgp",[52169,52170,52171,52172,47805,33549,52173,52174,52175,52176,27886,52177,2655,52178,52179,52180,29177,9,2669,52181,52182,52183,52184,52185,52186,52187,52188,52189,52190,52191,52192,52193,52194,52195,52196],"AI search engine","Next.js","open-source LLMs","Retrieval-Augmented Generation","vector database","transformer.js","semantic search","local LLM","search engine","programming","cost-effective AI","privacy-friendly AI","web application","web app","machine learning","natural language processing","NLP","semantic search engine","AI-powered search","AI-driven search","AI search","AI search engine development","AI search engine tutorial","AI search engine with Next.js","AI search engine with SQLite","AI search engine with open-source LLMs","AI search engine with transformer.js","AI search engine with RAG",{},"\u002F2025\u002F04\u002F05\u002Fbuilding-an-ai-search-engine-with-nextjs-and-free-open-source-llms","5th April 2025",{"title":47682,"description":52166},"2025\u002F04\u002F05\u002Fbuilding-an-ai-search-engine-with-nextjs-and-free-open-source-llms","hO2cjhdpoujVQkUlX3dMH59bA4FLlhxrQHrHZ7QTG4Q",{"id":52204,"title":52205,"articleTags":52206,"author":11,"blog":12,"body":52210,"description":52727,"extension":2649,"image":52728,"keywords":52729,"meta":52753,"navigation":515,"path":52754,"published":52755,"readTime":302,"seo":52756,"stem":52757,"type":2662,"__hash__":52758},"content\u002F2025\u002F04\u002F22\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build.md","How I Built a Budget-Friendly Custom Mechanical Keyboard (Part 1 - The Deep Dive Before the Build)",[52207,52208,52209],"Hobby","Tech","Entertainment",{"type":14,"value":52211,"toc":52713},[52212,52215,52229,52231,52235,52240,52243,52250,52253,52256,52259,52267,52274,52277,52283,52321,52328,52332,52339,52345,52401,52410,52414,52417,52421,52424,52430,52481,52485,52488,52502,52507,52511,52514,52520,52546,52549,52553,52556,52598,52603,52607,52621,52624,52650,52655,52660,52664,52667,52678,52681,52695,52698,52704,52707,52710],[17,52213,52205],{"id":52214},"how-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build",[27,52216,52217],{},[30,52218,52219,36,52221,40,52223],{},[33,52220],{"value":35},[33,52222],{"value":39},[42,52224,52225],{},[45,52226,52227],{"href":47},[33,52228],{"value":50},[52,52230],{":tags":54},[56,52232],{":audio-src":52233,":transcript-src":52234},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F04\u002F22\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F04\u002F22\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build\u002Fsummary.json",[27,52236,52237],{},[63,52238],{"alt":12847,"src":52239},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1745223262\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build_lc4bhn",[27,52241,52242],{},"I’ve been meaning to write this blog post for a few years now — I just never got around to it. But here we are. This is the story of how I got into custom mechanical keyboards and how that small curiosity turned into a full-blown obsession.",[27,52244,52245,52246,52249],{},"A few years back, I was bitten by the mechanical keyboard bug. At first, I thought, “",[42,52247,52248],{},"I’ll just buy a pre-built one and be done with it.","” No need to dive deep or spend too much time building one myself, right? Well… that was just the beginning.",[27,52251,52252],{},"My first keyboard came with brown tactile switches. I liked it at first, but as I started exploring the community and learning more, I realised those switches weren’t quite what I was after. So, I tried a new board with red linear switches. And then another one. And then I started swapping keycaps. And experimenting with sound. And trying out different sizes. Before I knew it, I was building and modding keyboards myself — chasing that perfect feel and sound.",[27,52254,52255],{},"In this blog article, I want to share my journey with you and give you a beginner-friendly intro to the world of custom mechanical keyboards. We’ll go over the basics — switches, keycaps, barebones kits, and everything in between. And by the end, we’ll even build a budget-friendly custom keyboard together.",[27,52257,52258],{},"Let’s jump in.",[104,52260,52262,52263,52266],{"id":52261},"choosing-the-right-size-what-feels-right","Choosing the Right Size — What ",[30,52264,52265],{},"Feels"," Right?",[27,52268,52269,52270,52273],{},"Before you start shopping for parts, it's good to understand the different keyboard sizes. There’s no “",[42,52271,52272],{},"best","” layout — it really comes down to what fits your workflow and desk space.",[27,52275,52276],{},"Here are the most common options:",[27,52278,52279],{},[63,52280],{"alt":52281,"src":52282},"Image of different keyboard sizes","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1745222456\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build\u002Fchoosing-the-right-size-what-feels-right_gdemas",[1003,52284,52285,52291,52297,52303,52309,52315],{},[1006,52286,52287,52290],{},[42,52288,52289],{},"Full-size (100%)",": Includes everything — letters, numbers, function row, arrow keys, and a number pad. Classic.",[1006,52292,52293,52296],{},[42,52294,52295],{},"Tenkeyless (TKL)",": Drops the number pad, which saves space but keeps the arrows and navigation keys.",[1006,52298,52299,52302],{},[42,52300,52301],{},"75%",": Similar to TKL but more compact. Everything’s smushed together a bit, which I personally love.",[1006,52304,52305,52308],{},[42,52306,52307],{},"65%",": Ditches the function row but keeps arrows and a few handy keys. A great balance of form and function.",[1006,52310,52311,52314],{},[42,52312,52313],{},"60%",": Minimalist vibes. Just the alphas, modifiers, and that's it. You’ll need layers for the rest.",[1006,52316,52317,52320],{},[42,52318,52319],{},"40% (and smaller)",": Ultra-compact. These are fun and quirky but come with a learning curve.",[27,52322,52323,52324,52327],{},"If you’re just getting started, I’d suggest trying a ",[42,52325,52326],{},"75% or TKL"," layout. You get most of the keys without the desk-hogging size of a full layout.",[104,52329,52331],{"id":52330},"what-actually-makes-a-mechanical-keyboard","What Actually Makes a Mechanical Keyboard?",[27,52333,52334,52335,52338],{},"A mechanical keyboard isn’t just about cool keycaps (though they ",[30,52336,52337],{},"do"," look awesome). Under the hood, there are a few main components:",[27,52340,52341],{},[63,52342],{"alt":52343,"src":52344},"Image of all main components of the keyboard","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1745222720\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build\u002Fwhat-actually-makes-a-mechanical-keyboard_ewxomd",[1003,52346,52347,52353,52365,52371,52377,52383,52389,52395],{},[1006,52348,52349,52352],{},[42,52350,52351],{},"Keycaps"," – What your fingers press. They come in different shapes and materials.",[1006,52354,52355,52358,52359,114,52362,1017],{},[42,52356,52357],{},"Switches"," – These sit under the keycaps and decide how your board ",[30,52360,52361],{},"feels",[30,52363,52364],{},"sounds",[1006,52366,52367,52370],{},[42,52368,52369],{},"Plate"," – A layer that holds your switches in place. It affects stiffness and acoustics.",[1006,52372,52373,52376],{},[42,52374,52375],{},"PCB (Printed Circuit Board)"," – The brains. Detects keystrokes and sends them to your computer.",[1006,52378,52379,52382],{},[42,52380,52381],{},"Stabilisers (Stabs)"," – These go under bigger keys (like spacebar and Enter) to keep them from wobbling.",[1006,52384,52385,52388],{},[42,52386,52387],{},"Foam or Dampening Layers"," – Optional, but highly recommended. These reduce vibration and improve sound.",[1006,52390,52391,52394],{},[42,52392,52393],{},"Case"," – Can be plastic (cheap and light), aluminum (solid and premium), or polycarbonate (soft and thocky).",[1006,52396,52397,52400],{},[42,52398,52399],{},"Cable"," – Usually detachable on customs. Bonus points if it’s coiled ☺️.",[3244,52402,52403],{},[27,52404,52405,52406,52409],{},"Most people start with a ",[42,52407,52408],{},"barebones kit",", which gives you the essentials: case, PCB, plate, and stabilisers. You bring the switches and keycaps to the party.",[104,52411,52413],{"id":52412},"lets-talk-keycaps-feel-sound-and-style","Let’s Talk Keycaps — Feel, Sound, and Style",[27,52415,52416],{},"Keycaps might seem like just a cosmetic choice, but they have a big impact on the way your board feels and sounds.",[123,52418,52420],{"id":52419},"profile-aka-shape-and-height","Profile (aka shape and height)",[27,52422,52423],{},"This is all about how each row is sculpted:",[27,52425,52426],{},[63,52427],{"alt":52428,"src":52429},"Image of keycaps profiles","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1745222454\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build\u002Fprofile-aka-shape-and-height_pssrng",[1003,52431,52432,52443,52457,52470],{},[1006,52433,52434,52437,52438],{},[42,52435,52436],{},"SA"," – Tall, retro-style, and heavily sculpted. They look cool but can take some getting used to.\n",[1003,52439,52440],{},[1006,52441,52442],{},"Height: ~16.3 mm",[1006,52444,52445,52448,52449],{},[42,52446,52447],{},"OEM"," – The standard shape used on most pre-built keyboards. Slightly sculpted and comfortable for general use.\n",[1003,52450,52451],{},[1006,52452,52453,52456],{},[30,52454,52455],{},"Height",": ~11.9 mm",[1006,52458,52459,52462,52463],{},[42,52460,52461],{},"Cherry"," – Shorter than OEM, with a smoother curve. Many enthusiasts love this profile for its comfort.\n",[1003,52464,52465],{},[1006,52466,52467,52469],{},[30,52468,52455],{},": ~9.4 mm",[1006,52471,52472,52475,52476],{},[42,52473,52474],{},"DSA\u002FXDA"," – Flat profiles with a uniform height. I personally love these — especially XDA. They’re clean and minimalist.\n",[1003,52477,52478],{},[1006,52479,52480],{},"Height: ~7.6 mm (DSA), ~9.1 mm (XDA)",[123,52482,52484],{"id":52483},"material","Material",[27,52486,52487],{},"Two main types here:",[1003,52489,52490,52496],{},[1006,52491,52492,52495],{},[42,52493,52494],{},"ABS"," – Smooth and light. More prone to shine over time but often more colourful.",[1006,52497,52498,52501],{},[42,52499,52500],{},"PBT"," – Tougher and more textured. Usually sounds deeper and lasts longer.",[3244,52503,52504],{},[27,52505,52506],{},"For me, it’s XDA + PBT all day. I just love the feel, and the sound is super satisfying.",[104,52508,52510],{"id":52509},"inside-the-switch-whats-going-on-under-there","Inside the Switch — What’s Going On Under There?",[27,52512,52513],{},"A switch might be small, but it’s doing a lot. Here’s what’s inside:",[27,52515,52516],{},[63,52517],{"alt":52518,"src":52519},"Image of all main components of the switch","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1745222454\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build\u002Finside-the-switch-whats-going-on-under-there_rsmrpi",[1003,52521,52522,52528,52534,52540],{},[1006,52523,52524,52527],{},[42,52525,52526],{},"Top Housing"," – The outer shell that holds it all together.",[1006,52529,52530,52533],{},[42,52531,52532],{},"Stem"," – The coloured bit that moves up and down when you type.",[1006,52535,52536,52539],{},[42,52537,52538],{},"Spring"," – Gives the switch resistance and bounce.",[1006,52541,52542,52545],{},[42,52543,52544],{},"Bottom Housing"," – Connects the switch to your PCB.",[27,52547,52548],{},"Simple enough, right? But small design changes can totally change how a switch feels.",[104,52550,52552],{"id":52551},"switch-types-linear-tactile-or-clicky","Switch Types — Linear, Tactile, or Clicky?",[27,52554,52555],{},"When people talk switches, they usually mean one of these three:",[1003,52557,52558,52572,52585],{},[1006,52559,52560,52563,52564],{},[42,52561,52562],{},"Linear"," – Smooth from top to bottom. No bump, no click. Great for fast typing and gaming.\n",[1003,52565,52566],{},[1006,52567,52568,52571],{},[30,52569,52570],{},"Most popular",": Gateron Red, Cherry MX Red, Kailh Red",[1006,52573,52574,52577,52578],{},[42,52575,52576],{},"Tactile"," – Has a little bump mid-press that lets you know the key’s registered.\n",[1003,52579,52580],{},[1006,52581,52582,52584],{},[30,52583,52570],{},": Gateron Brown, Cherry MX Brown, Zealios V2",[1006,52586,52587,52590,52591],{},[42,52588,52589],{},"Clicky"," – Like tactile, but louder. Click sound and all.\n",[1003,52592,52593],{},[1006,52594,52595,52597],{},[30,52596,52570],{},": Gateron Blue, Cherry MX Blue, Kailh Box White",[3244,52599,52600],{},[27,52601,52602],{},"I started on tactiles, but eventually I fell in love with linears. There’s something super buttery about them — especially when lubed.",[104,52604,52606],{"id":52605},"understanding-switch-force","Understanding Switch Force",[27,52608,52609,52610,3955,52613,52616,52617,52620],{},"When exploring mechanical switches, you'll see measurements like ",[42,52611,52612],{},"45g",[42,52614,52615],{},"62g",". That \"g\" stands for ",[42,52618,52619],{},"grams of actuation force"," — the pressure needed to register a keystroke.",[27,52622,52623],{},"Here's what different actuation forces feel like:",[1003,52625,52626,52632,52638,52644],{},[1006,52627,52628,52631],{},[42,52629,52630],{},"\u003C 45g – Ultra-Light:"," Feather-light touch. Perfect for fast typing but might cause accidental keypresses.",[1006,52633,52634,52637],{},[42,52635,52636],{},"45–55g – Light:"," Smooth and effortless. Most popular for beginners and everyday typing.",[1006,52639,52640,52643],{},[42,52641,52642],{},"55–65g – Medium:"," Balanced resistance. Offers precise control without finger fatigue.",[1006,52645,52646,52649],{},[42,52647,52648],{},"65g+ – Heavy:"," Firm resistance. Great for heavy-handed typists who want to prevent misclicks.",[3244,52651,52652],{},[27,52653,52654],{},"For beginners, I suggest switches in the 45-55g range. This sweet spot lets you comfortably develop your typing style.",[3244,52656,52657],{},[27,52658,52659],{},"Everyone has a preference — and trying different switches is part of the fun. If possible, grab a switch tester (there are plenty available to order from various manufacturers) or use a hot-swappable keyboard to experiment. Feeling the differences firsthand is the best way to figure out what suits your typing style.",[104,52661,52663],{"id":52662},"lets-talk-sound-what-is-a-thocky-keyboard","Let’s Talk Sound — What is a “Thocky” Keyboard?",[27,52665,52666],{},"Ah yes — the famous thock. If you hang around keyboard nerds long enough, you’ll hear a lot of sound talk.",[1003,52668,52669,52672,52675],{},[1006,52670,52671],{},"Thock – Deep, satisfying, and soft. The holy grail.",[1006,52673,52674],{},"Clack – Sharper and brighter. Think crisp clicks.",[1006,52676,52677],{},"Hollow – That cheap, echoey sound you don’t want.",[27,52679,52680],{},"What affects sound?",[1003,52682,52683,52686,52689,52692],{},[1006,52684,52685],{},"Switch type – Linears are usually quieter. Clickies are loud and proud.",[1006,52687,52688],{},"Keycaps – PBT tends to sound deeper.",[1006,52690,52691],{},"Case material – Aluminum sounds solid; plastic is lighter and often more hollow.",[1006,52693,52694],{},"Mods – Adding foam, lubing switches, taping the PCB — these can transform your board.",[27,52696,52697],{},"The fun part? You can experiment endlessly until it sounds just right.",[104,52699,52701],{"id":52700},"ready-to-build-your-first-budget-custom-keyboard",[42,52702,52703],{},"Ready to Build Your First Budget Custom Keyboard?",[27,52705,52706],{},"We’ve covered a lot, and you’re probably itching to start building your own board now — and you totally should! In the next section (coming soon), I’ll walk you through building a beginner-friendly mechanical keyboard from a barebones kit, all without breaking the bank.",[27,52708,52709],{},"Trust me: it’s way easier than it seems — and way more fun than you expect.",[27,52711,52712],{},"Let me know if you want help picking parts or figuring out a build that works for your budget. I’m always down to geek out about keyboards!",{"title":133,"searchDepth":173,"depth":173,"links":52714},[52715,52717,52718,52722,52723,52724,52725,52726],{"id":52261,"depth":173,"text":52716},"Choosing the Right Size — What Feels Right?",{"id":52330,"depth":173,"text":52331},{"id":52412,"depth":173,"text":52413,"children":52719},[52720,52721],{"id":52419,"depth":188,"text":52420},{"id":52483,"depth":188,"text":52484},{"id":52509,"depth":173,"text":52510},{"id":52551,"depth":173,"text":52552},{"id":52605,"depth":173,"text":52606},{"id":52662,"depth":173,"text":52663},{"id":52700,"depth":173,"text":52703},"Curious about custom mechanical keyboards but not sure where to start? This beginner-friendly guide walks you through all the essential basics—from switch types to keycap profiles and materials. You’ll learn what a mechanical keyboard is made of, understand its core components, and get the knowledge you need before shopping for your first build.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1745223262\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build_lc4bhn",[52730,52731,52732,52733,52734,52735,52736,52737,52738,52739,52740,52741,52742,52743,52744,52745,52746,52747,52748,52749,52750,52751,52752],"custom mechanical keyboard","budget mechanical keyboard","mechanical keyboard for beginners","how to build a mechanical keyboard","mechanical keyboard switches explained","keycap profiles guide","keyboard layouts 100% TKL 75% 65% 60% 40%","mechanical keyboard sound thocky","linear vs tactile vs clicky switches","beginner keyboard build guide","DIY mechanical keyboard","affordable mechanical keyboard build","switch actuation force","best keycap materials","hot-swappable keyboard guide","keyboard stabilisers explained","keyboard build tools","mechanical keyboard sound dampening","keyboard build tips","keyboard build guide","mechanical keyboard build process","mechanical keyboard build tutorial","mechanical keyboard build guide for beginners",{},"\u002F2025\u002F04\u002F22\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build","22nd April 2025",{"title":52205,"description":52727},"2025\u002F04\u002F22\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-1-the-deep-dive-before-the-build","nX1y85mARI8De53r3tlEXFh6Nau73-jCHYKRYvl_tNI",{"id":52760,"title":52761,"articleTags":52762,"author":11,"blog":12,"body":52763,"description":53507,"extension":2649,"image":53508,"keywords":53509,"meta":53523,"navigation":515,"path":53524,"published":53525,"readTime":1568,"seo":53526,"stem":53527,"type":2662,"__hash__":53528},"content\u002F2025\u002F07\u002F06\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together.md","How I Built a Budget-Friendly Custom Mechanical Keyboard (Part 2 - Building It Together)",[52209,52207,52208],{"type":14,"value":52764,"toc":53486},[52765,52768,52782,52784,52788,52793,52803,52810,52813,52816,52820,52823,52827,52840,52843,52863,52879,52901,52908,52917,52922,52926,52972,52978,52982,52992,52997,53000,53004,53007,53023,53029,53036,53043,53049,53053,53056,53093,53096,53102,53106,53109,53112,53120,53126,53132,53139,53142,53146,53149,53166,53172,53176,53179,53182,53190,53196,53201,53209,53215,53218,53222,53225,53228,53233,53239,53244,53250,53255,53261,53267,53272,53278,53281,53285,53288,53293,53299,53307,53310,53316,53320,53323,53329,53332,53365,53368,53374,53377,53381,53384,53387,53390,53396,53400,53403,53409,53412,53423,53428,53432,53435,53446,53449,53461,53467,53470,53474,53477,53480,53483],[17,52766,52761],{"id":52767},"how-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together",[27,52769,52770],{},[30,52771,52772,36,52774,40,52776],{},[33,52773],{"value":35},[33,52775],{"value":39},[42,52777,52778],{},[45,52779,52780],{"href":47},[33,52781],{"value":50},[52,52783],{":tags":54},[56,52785],{":audio-src":52786,":transcript-src":52787},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F07\u002F06\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F07\u002F06\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002Fsummary.json",[27,52789,52790],{},[63,52791],{"alt":12847,"src":52792},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751714544\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002Fcover-image_ovafxl",[27,52794,52795,52796,52799,52800,1017],{},"If you've landed here and haven't read ",[47718,52797,52798],{"to":52754},"Part 1",", I highly recommend checking it out first. That's where I laid the groundwork — diving deep into keyboard sizes, keycap profiles, switch types, and all the essential components that make a mechanical keyboard feel ",[30,52801,52802],{},"just right",[27,52804,52805,52806,52809],{},"Now comes the exciting part — putting all that knowledge to use! This is where things get ",[30,52807,52808],{},"real",". We're going to build a custom mechanical keyboard from scratch that's budget-friendly, beginner-friendly, and still super satisfying to type on.",[27,52811,52812],{},"Whether this is your first build or you're just curious about the process, I'll walk you through everything step by step. From unboxing the parts to installing switches, lubing both switches and stabilisers, mounting keycaps — and even doing a few fun mods to improve sound and feel — it's all here.",[27,52814,52815],{},"Let's get our hands dirty and start building our dream keyboard.",[104,52817,52819],{"id":52818},"the-parts-i-picked-and-why","The Parts I Picked (And Why)",[27,52821,52822],{},"I wanted to build something that wouldn't break the bank but would still feel amazing to type on and look great on my desk. I'll walk you through all the components I chose, including where to buy them and how much they cost. I'll also cover some optional modifications that can improve your typing experience and sound quality. Here's my complete parts list:",[123,52824,52826],{"id":52825},"essential-parts","Essential Parts",[1003,52828,52829],{},[1006,52830,52831,726,52834,52839],{},[42,52832,52833],{},"Barebones Kit",[45,52835,52838],{"href":52836,"target":2716,"rel":52837},"https:\u002F\u002Fkineticlabs.com\u002Fkeyboards\u002Fkinetic\u002Fgmk87-keyboard",[2718,2719],"GMK87 TKL Wireless Mechanical Keyboard"," – This keyboard is incredibly budget-friendly for the features it offers. At just $69.99, it's one of the best value kits out there. The GMK87 uses a Tenkeyless (TKL) layout with a lightweight plastic case that helps achieve that sought-after thocky sound more easily. It supports both Bluetooth and wired connectivity and includes a hot-swappable PCB, allowing you to install your own switches with ease.",[27,52841,52842],{},"It also features:",[1003,52844,52845,52848,52851,52854,52857,52860],{},[1006,52846,52847],{},"85 keys",[1006,52849,52850],{},"A volume control knob",[1006,52852,52853],{},"A programmable LCD screen (we'll explore how to customise this with our own animation later!)",[1006,52855,52856],{},"Pre-installed, pre-lubed plate-mounted stabilisers (which we'll improve even more with extra lube)",[1006,52858,52859],{},"A gasket-mounted structure that provides a softer typing feel and reduces vibrations",[1006,52861,52862],{},"Sound-dampening foam for a better acoustic experience",[27,52864,52865,52866,164,52869,164,52872,14528,52875,52878],{},"It comes in four color options: ",[42,52867,52868],{},"Toffee",[42,52870,52871],{},"Pastel Green",[42,52873,52874],{},"Black",[42,52876,52877],{},"White",". I went with the White version for a clean aesthetic.",[1003,52880,52881,52891],{},[1006,52882,52883,726,52885,52890],{},[42,52884,52357],{},[45,52886,52889],{"href":52887,"target":2716,"rel":52888},"https:\u002F\u002Fen.akkogear.com\u002Fproduct\u002Fakko-v3-cream-yellow-pro-switch-45pcs",[2718,2719],"Akko V3 Creamy Yellow Pro (90 pcs)"," – These are an excellent option for anyone looking to build a smooth, responsive keyboard without spending a fortune. For just $17.98, you get 90 linear switches with a 50g actuation force — perfect for beginners or anyone who doesn't want to press too hard while typing. They come factory-lubed, but I'll be lubing them myself for an even smoother and quieter experience.",[1006,52892,52893,726,52895,52900],{},[42,52894,52351],{},[45,52896,52899],{"href":52897,"target":2716,"rel":52898},"https:\u002F\u002Fwww.aliexpress.com\u002Fitem\u002F1005006871052624.html",[2718,2719],"Budget XDA PBT Set from AliExpress"," – Clean, minimalist, and surprisingly high quality for just $26.33. This particular set comes with 95 keys and features adorable cat and dog illustrations, adding a playful touch to the build. Made from durable PBT plastic, they're built to last. I chose the blue and white combo, but other color options are available too.",[27,52902,52903,52904,52907],{},"💸 ",[42,52905,52906],{},"Total Cost",": ~$114.30 USD",[27,52909,52910,52911,52916],{},"Of course, the price may vary depending on your country, taxes, and shipping fees — but considering that many prebuilt keyboards with similar features can cost the same or even more, this is a fantastic deal. For example, the ",[45,52912,52915],{"href":52913,"target":2716,"rel":52914},"https:\u002F\u002Fkeychron.com.au\u002Fproducts\u002Fkeychron-k8-qmk-wireless-mechanical-keyboard-version-2",[2718,2719],"Keychron K8"," with a similar layout and feature set costs around $179 USD.",[3244,52918,52919],{},[27,52920,52921],{},"Note: Prices mentioned are accurate as of May 2025 but may change in the future.",[123,52923,52925],{"id":52924},"optional-add-ons-to-improve-sound-typing-feel","Optional Add-ons to Improve Sound & Typing Feel",[1003,52927,52928,52944,52954,52960,52966],{},[1006,52929,52930,52933,52934,52939,52940,52943],{},[42,52931,52932],{},"Lubing Supplies"," – ",[45,52935,52938],{"href":52936,"target":2716,"rel":52937},"https:\u002F\u002Fwww.amazon.com\u002FKrytox-GPL-205-G0\u002Fdp\u002FB08MCK6QL3",[2718,2719],"Krytox 205g0"," for switches and dielectric grease for stabilisers. Krytox 205g0 is a favourite in the mechanical keyboard community. It costs about $23.70 for 10g, but a small amount goes a long way — I've used it on three sets of switches\u002Fstabilisers and still have about half left. Just make sure to get ",[42,52941,52942],{},"205g0",", not other Krytox variations, as it's ideal for linear switches.",[1006,52945,52946,52949,52950,52953],{},[42,52947,52948],{},"Tools"," – You'll also want a switch opener, a small brush for lubing, and patience (lots of it 🙂). You can find these tools cheaply on AliExpress—just search for ",[42,52951,52952],{},"lubing switches kit",". They cost around $10 and are a one-time investment since you'll use them for other switches in the future. They don't expire 😜!",[1006,52955,52956,52959],{},[42,52957,52958],{},"Switch Films"," – Optional, but they can tighten up the switch housing and enhance the sound. These are available on AliExpress and cost as little as $5 to $10 per 120 pieces.",[1006,52961,52962,52965],{},[42,52963,52964],{},"Mods"," – Synthetic filling (commonly known as fiberfill or Poly-Fil) inside the case and tape for the back of the PCB (for the \"tape mod\") can make a noticeable difference in acoustics and feel. These materials can be found at local hardware stores, craft shops, or office supply stores in most countries.",[1006,52967,52968,52971],{},[42,52969,52970],{},"Coiled cable"," – I chose a light blue coiled cable to match the keycaps from AliExpress. A coiled cable not only looks good but also helps keep your desk tidy by preventing excess cable clutter. It doesn't need to be expensive — you can find a USB Type-C cable for as little as $10.",[27,52973,52974],{},[63,52975],{"alt":52976,"src":52977},"all the parts laid out on the table before the build","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715102\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1657_jxcste",[104,52979,52981],{"id":52980},"prepping-the-stabilisers","Prepping the Stabilisers",[27,52983,52984,52985,164,52988,52991],{},"Stabilisers sit under your bigger keys (like spacebar, ",[42,52986,52987],{},"Enter",[42,52989,52990],{},"Shift",") and keep them from wobbling or sounding rattly. Lubing the stabilisers is a tiny mod, but it makes a huge difference in how your larger keys such as spacebar or Enter key feel.",[3244,52993,52994],{},[27,52995,52996],{},"Before I walk you through how I did it, I should mention that stabilisers need to be carefully detached from the board if they come pre-installed in the barebones kit (which is the case with our kit). Also, every barebones kit is different and needs a different approach to remove stabilisers. What works for this board might not work for others.",[27,52998,52999],{},"We first need to reach the stabilisers, which means opening up the keyboard case. Here's how I did it, and what you'll want to watch out for:",[123,53001,53003],{"id":53002},"opening-the-case","Opening the Case",[27,53005,53006],{},"There were no screws on my GMK87 — the entire case is held together by friction clips. To open it:",[2569,53008,53009,53016],{},[1006,53010,53011,53012,53015],{},"I used a ",[42,53013,53014],{},"flat-head screwdriver"," (not a cross-head\u002FPhillips) and gently inserted it into the seam between the top and bottom case halves.",[1006,53017,53018,53019,53022],{},"Then I applied a bit of upward pressure to ",[42,53020,53021],{},"pop the clips open"," — carefully working my way around the edges.",[27,53024,53025],{},[63,53026],{"alt":53027,"src":53028},"opening the case keyboard with a flat-head screwdriver","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715106\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1704.HEIC_mh94mq",[3244,53030,53031],{},[27,53032,53033],{},[30,53034,53035],{},"Tip: Be patient and go slow to avoid damaging the plastic.",[27,53037,53038,53039,53042],{},"Once the top comes off, you'll notice ",[42,53040,53041],{},"two cables"," inside — one for the LCD screen and one for the knob. Carefully unplug both connectors before separating the case completely.",[27,53044,53045],{},[63,53046],{"alt":53047,"src":53048},"showing two cables inside the keyboard case","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715106\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1707.HEIC_opgj80",[123,53050,53052],{"id":53051},"inside-the-case","Inside the Case",[27,53054,53055],{},"With the case open, you'll see the build structure consists of multiple layers:",[1003,53057,53058,53063,53069,53075,53081,53087],{},[1006,53059,53060],{},[42,53061,53062],{},"Top case",[1006,53064,53065,53068],{},[42,53066,53067],{},"Silicone pad"," – dampens vibrations and reduces hollowness.",[1006,53070,53071,53074],{},[42,53072,53073],{},"Plate foam"," – helps with acoustics and overall sound profile.",[1006,53076,53077,53080],{},[42,53078,53079],{},"PCB"," – the main board that registers keystrokes.",[1006,53082,53083,53086],{},[42,53084,53085],{},"Switch plate"," – holds the switches firmly in place and affects how the keyboard feels when typing.",[1006,53088,53089,53092],{},[42,53090,53091],{},"Bottom case foam"," – adds even more dampening to the sound.",[27,53094,53095],{},"Honestly, I was pleasantly surprised — the out-of-the-box sound treatment in this keyboard is quite good. But don't worry, we'll still take it further with some extra mods later.",[27,53097,53098],{},[63,53099],{"alt":53100,"src":53101},"Top case, Silicone pad, Plate foam, PCB, Switch plate, Bottom case foam","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715105\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1712.HEIC_tq9t2x",[123,53103,53105],{"id":53104},"removing-the-stabilisers","Removing the Stabilisers",[27,53107,53108],{},"Now that the PCB and plate are exposed, we can remove the stabilisers. These are plate-mounted and secured using plastic clips on both sides.",[27,53110,53111],{},"Here's how I removed them:",[2569,53113,53114,53117],{},[1006,53115,53116],{},"I used a pair of long-nose pliers to gently press in the clips on each side of the stabiliser.",[1006,53118,53119],{},"Once the clips were pressed, I carefully pulled the stabilisers out from the plate.",[27,53121,53122],{},[63,53123],{"alt":53124,"src":53125},"used a pair of long-nose pliers to gently press in the clips on each side of the stabiliser","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751722756\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1714.HEIC_fny8ng",[27,53127,53128],{},[63,53129],{"alt":53130,"src":53131},"once the clips were pressed, I carefully pulled the stabilisers out from the plate","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715105\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1715.HEIC_ikzfsk",[3244,53133,53134],{},[27,53135,53136],{},[30,53137,53138],{},"Tip: Take your time and don't force it — the plastic can crack if you go too hard.",[27,53140,53141],{},"Once you've got the stabilisers out, it's time to give them the love they deserve — let's lube!",[123,53143,53145],{"id":53144},"lubing-the-stabilisers","Lubing the Stabilisers",[27,53147,53148],{},"When lubing stabilisers:",[1003,53150,53151,53154,53157,53160,53163],{},[1006,53152,53153],{},"First, remove the plastic housings from both ends so you can fully access the wire.",[1006,53155,53156],{},"Apply a small amount of dielectric grease to the ends of the wire.",[1006,53158,53159],{},"Use a fine brush or toothpick for a clean and precise application.",[1006,53161,53162],{},"You can also lightly coat the inside of the housing where the sliders move.",[1006,53164,53165],{},"Once lubed, reassemble and reinstall the stabilisers by pressing them gently back into place on the plate until they snap in securely.",[27,53167,53168],{},[63,53169],{"alt":53170,"src":53171},"lubing the stabilisers with dielectric grease","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715108\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1726_rko5vt",[104,53173,53175],{"id":53174},"modding-the-case","Modding the Case",[27,53177,53178],{},"While the board is still open, we can make a few simple tweaks to improve the overall sound and typing feel. These mods are entirely optional, but if you're a fan of that deep, clean \"thock\" sound, you'll definitely want to try them.",[27,53180,53181],{},"Here's what I did:",[1003,53183,53184],{},[1006,53185,53186,53189],{},[42,53187,53188],{},"Tape Mod"," – I applied three layers of masking tape to the back of the PCB. This mod is extremely popular in the keyboard community because it makes the board sound deeper and more refined when typing. Just make sure the tape doesn't cover any important components or connectors.",[27,53191,53192],{},[63,53193],{"alt":53194,"src":53195},"tape mod on the back of the PCB","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715110\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1744.HEIC_kszdyj",[27,53197,53198],{},[63,53199],{"alt":53194,"src":53200},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715114\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1745.HEIC_mqp5ba",[1003,53202,53203],{},[1006,53204,53205,53208],{},[42,53206,53207],{},"Case Stuffing with Poly-Fil"," – To reduce empty space and eliminate hollow sound, I added some synthetic stuffing (Poly-Fil) between the PCB and the silicone pad. This soft material fills the space without putting pressure on components, and it really helps tighten up the sound profile.",[27,53210,53211],{},[63,53212],{"alt":53213,"src":53214},"case stuffing with Poly-Fil","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715109\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1883.HEIC_nxsrau",[27,53216,53217],{},"These two quick mods made a noticeable difference—the keyboard now sounds deeper, more solid, and much less plasticky. We can now close the case as it was before.",[104,53219,53221],{"id":53220},"lubing-your-switches-optional-but-worth-it","Lubing Your Switches (Optional but Worth It)",[27,53223,53224],{},"This step is technically optional, but if you're chasing that buttery, \"thocky\" perfection, lubing your switches is 100% worth the extra effort.",[27,53226,53227],{},"How I lubed mine:",[1003,53229,53230],{},[1006,53231,53232],{},"I opened each switch using a switch opener (you can also use a flathead screwdriver in a pinch, but the proper tool makes life easier).",[27,53234,53235],{},[63,53236],{"alt":53237,"src":53238},"switch opener with open switch inside","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715111\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1907.HEIC_t5ifjc",[1003,53240,53241],{},[1006,53242,53243],{},"I applied a thin layer of Krytox 205g0 to the stem rails, spring, and bottom housing, being careful not to overlube. A light coat is all you need.",[27,53245,53246],{},[63,53247],{"alt":53248,"src":53249},"lubing the switch with Krytox 205g0","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715108\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1889.HEIC_y59ecb",[1003,53251,53252],{},[1006,53253,53254],{},"Before closing the switch, I also added switch films between the top and bottom housings. As mentioned earlier, switch films help tighten the housing, reduce wobble, and give the switch a cleaner, more consistent sound.",[27,53256,53257],{},[63,53258],{"alt":53259,"src":53260},"switch films and switch in the background","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715110\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1900.HEIC_vitgkl",[27,53262,53263],{},[63,53264],{"alt":53265,"src":53266},"switch films installed in the switch","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715110\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1902.HEIC_a2hlo7",[1003,53268,53269],{},[1006,53270,53271],{},"Once everything was lubed and the films were in place, I carefully reassembled each switch and set them aside, ready for installation.",[27,53273,53274],{},[63,53275],{"alt":53276,"src":53277},"switch ready for installation","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715115\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1910_2.HEIC_dqfwy4",[27,53279,53280],{},"The process is repetitive but oddly calming. It took about 1 hour for 15 switches, but it's surprisingly relaxing. I put on a podcast and just zoned out. The final result made the keyboard feel and sound so much better.",[104,53282,53284],{"id":53283},"installing-the-switches","Installing the Switches",[27,53286,53287],{},"It was finally time to start putting everything together. Here's how I approached it:",[1003,53289,53290],{},[1006,53291,53292],{},"Line up the switch pins carefully with the holes on the PCB. Make sure both metal pins are straight and facing downward before applying any pressure. You'll feel a soft \"click\" when they snap into place.",[27,53294,53295],{},[63,53296],{"alt":53297,"src":53298},"installing the switch into the PCB","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715113\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1911.HEIC_qqcxh2",[1003,53300,53301,53304],{},[1006,53302,53303],{},"Be gentle—don't force it. If you push too hard or at the wrong angle, you might bend the pins. Bent pins are the most common cause of dead or unresponsive keys. (If that happens, don't worry—they're usually fixable with a steady hand and tweezers.)",[1006,53305,53306],{},"As you go, take a moment to check that each switch is seated evenly and fully pressed in.",[27,53308,53309],{},"Once all the switches were installed, I gave the board a final visual inspection to ensure everything was aligned properly.",[27,53311,53312],{},[63,53313],{"alt":53314,"src":53315},"all switches installed in the PCB","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715115\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1971.HEIC_v2ossj",[104,53317,53319],{"id":53318},"adding-keycaps","Adding Keycaps",[27,53321,53322],{},"This is the step where your keyboard really starts to come to life.",[27,53324,53325],{},[63,53326],{"alt":53327,"src":53328},"keycaps ready for installation","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715103\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1675_arfgpw",[27,53330,53331],{},"Installing keycaps is a pretty straightforward process:",[1003,53333,53334,53337,53348,53358],{},[1006,53335,53336],{},"Press them on row by row, making sure each one clicks firmly onto the switch stem.",[1006,53338,53339,53340,53343,53344,53347],{},"Since we're using ",[42,53341,53342],{},"XDA"," profile keycaps, we don't need to worry about row-specific placement—they're uniform in shape and height. The same would apply to ",[42,53345,53346],{},"DSA"," profile caps as well.",[1006,53349,53350,53351,164,53353,29088,53355,53357],{},"However, if you're using a sculpted profile like ",[42,53352,52447],{},[42,53354,52461],{},[42,53356,52436],{},", make sure you're placing each keycap on the correct row. Putting sculpted keycaps in the wrong position will not only look off, but it can also feel uncomfortable when typing.",[1006,53359,53360,53361,53364],{},"If you're not sure what keycap profile you're using, or how profiles work, I covered that in detail in ",[47718,53362,53363],{"to":52754},"Part 1 of this blog series","—feel free to check that out for a quick refresher.",[27,53366,53367],{},"Once all the keycaps are on, give the board a gentle press test—every key should feel secure and snappy. It's a great moment to admire your build before the final step.",[27,53369,53370],{},[63,53371],{"alt":53372,"src":53373},"keycaps installed on the keyboard","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715112\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1972_mb1nfw",[27,53375,53376],{},"Up next: plug and play. Let's power it on and see how it performs!",[104,53378,53380],{"id":53379},"plug-it-in-test-it-out","Plug It In & Test It Out",[27,53382,53383],{},"Now that the keyboard is fully assembled, it's time for the most exciting part: powering it on and seeing it in action!",[27,53385,53386],{},"First, plug in the cable. The keyboard comes with a standard USB-C cable, but since I'm a big fan of coiled cables, I decided to swap it out for a budget-friendly coiled one that matches the board's aesthetic. (Totally optional—it's more of a visual upgrade.)",[27,53388,53389],{},"Coiled cables usually come in two parts: the spiral coil and a straight section, connected by a metal aviator connector, which gives it that premium custom look.",[27,53391,53392],{},[63,53393],{"alt":53394,"src":53395},"coiled cable with aviator connector","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751715107\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1686_ckbqup",[123,53397,53399],{"id":53398},"a-quick-note-before-powering-on","A quick note before powering on:",[27,53401,53402],{},"Don't forget to remove the protective film on the little LCD screen in the top-right corner, right next to the knob. It's easy to miss.",[27,53404,53405],{},[63,53406],{"alt":53407,"src":53408},"removing protective film on the LCD screen","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751763152\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_1973_nkannv",[27,53410,53411],{},"This particular board has a few cool screen features built in:",[1003,53413,53414,53417,53420],{},[1006,53415,53416],{},"It can display the current time.",[1006,53418,53419],{},"There are two built-in GIF animations to choose from.",[1006,53421,53422],{},"You can even upload your own custom animation through software—which is a fun way to personalise your board even further.",[3244,53424,53425],{},[27,53426,53427],{},"In my experience, the only software available was for Windows, despite what the keyboard documentation stated. I couldn't find any reference to Mac software. If you happen to find software that works on Mac for customsing the GIF animation, please email me.",[123,53429,53431],{"id":53430},"rgb-backlight-features","RGB Backlight Features",[27,53433,53434],{},"The GMK87 also comes with customisable backlighting, including:",[1003,53436,53437,53440,53443],{},[1006,53438,53439],{},"Adjustable brightness and animation speed",[1006,53441,53442],{},"Multiple lighting effects (breathing, wave, ripple, etc.)",[1006,53444,53445],{},"Full control over hue, saturation, and colour mode",[27,53447,53448],{},"Feel free to play around with the settings to match your mood or setup.",[27,53450,53451,53452,164,53455,14528,53458,1017],{},"Once I had everything plugged in, I ran a quick switch test and... wow. The keyboard felt and sounded exactly how I'd imagined—",[42,53453,53454],{},"thocky",[42,53456,53457],{},"crisp",[42,53459,53460],{},"smooth",[27,53462,53463],{},[63,53464],{"alt":53465,"src":53466},"keyboard powered on and ready to use","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1751763259\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002FIMG_2015_nyuyju",[27,53468,53469],{},"Mission accomplished. 🎉",[104,53471,53473],{"id":53472},"final-thoughtsyou-built-a-keyboard","Final Thoughts—You Built a Keyboard!",[27,53475,53476],{},"Congrats—you just built your own custom mechanical keyboard!",[27,53478,53479],{},"It's one thing to buy a keyboard off the shelf. It's another thing entirely to build one with your own hands, tweak it, mod it, and make it yours. And the best part? You can always improve it. Try different switches, keycaps, or foam mods—your journey is just beginning.",[27,53481,53482],{},"If you've been thinking about doing your own build, I say go for it. Start small, experiment, and don't worry about getting everything perfect the first time. That's half the fun.",[27,53484,53485],{},"Let me know if you want help tuning the final sound, picking your next switch set, or diving deeper into more advanced mods (like case foam or custom firmware). I've got plenty of keyboard rabbit holes we can explore together.",{"title":133,"searchDepth":173,"depth":173,"links":53487},[53488,53492,53498,53499,53500,53501,53502,53506],{"id":52818,"depth":173,"text":52819,"children":53489},[53490,53491],{"id":52825,"depth":188,"text":52826},{"id":52924,"depth":188,"text":52925},{"id":52980,"depth":173,"text":52981,"children":53493},[53494,53495,53496,53497],{"id":53002,"depth":188,"text":53003},{"id":53051,"depth":188,"text":53052},{"id":53104,"depth":188,"text":53105},{"id":53144,"depth":188,"text":53145},{"id":53174,"depth":173,"text":53175},{"id":53220,"depth":173,"text":53221},{"id":53283,"depth":173,"text":53284},{"id":53318,"depth":173,"text":53319},{"id":53379,"depth":173,"text":53380,"children":53503},[53504,53505],{"id":53398,"depth":188,"text":53399},{"id":53430,"depth":188,"text":53431},{"id":53472,"depth":173,"text":53473},"Follow this detailed, step-by-step guide to building your own budget-friendly custom mechanical keyboard using the GMK87 TKL, Akko Cream Yellow switches, and XDA PBT keycaps. Learn how to mod stabilisers, lube switches, apply the tape mod, install a coiled cable, and enhance sound and typing feel—perfect for beginners and enthusiasts looking to create a thocky, responsive keyboard without breaking the bank.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1751714544\u002Fblog\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together\u002Fcover-image_ovafxl",[53510,53511,53512,53513,53514,53515,53516,53517,53518,53519,53520,53521,53522,52730,52731,52732,52733,52734,52735,52736,52737,52738,52739,52740,52741,52742,52743,52744,52745,52746,52747,52748,52749,52750,52751,52752],"DIY keyboard build","keyboard modding","lubing switches","stabiliser mod","GMK87","Akko Cream Yellow Pro","coiled cable","XDA keycaps","beginner keyboard build","thocky sound","tape mod","switch films","mechanical keyboard tutorial",{},"\u002F2025\u002F07\u002F06\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together","6th July 2025",{"title":52761,"description":53507},"2025\u002F07\u002F06\u002Fhow-i-built-a-budget-friendly-custom-mechanical-keyboard-part-2-building-it-together","-MBzIC3VdEvm0hQNw4Dkrx0GgdRZD24FJi6JKogn5LA",{"id":53530,"title":53531,"articleTags":53532,"author":11,"blog":12,"body":53533,"description":54240,"extension":2649,"image":54241,"keywords":54242,"meta":54253,"navigation":515,"path":54254,"published":54255,"readTime":278,"seo":54256,"stem":54257,"type":2662,"__hash__":54258},"content\u002F2025\u002F09\u002F21\u002Fthe-right-way-to-handle-hover-in-css-across-devices.md","The Right Way to Handle Hover in CSS Across Devices",[7531,10,9],{"type":14,"value":53534,"toc":54225},[53535,53538,53552,53554,53558,53563,53566,53569,53573,53593,53603,53606,53712,53715,53722,53725,53808,53870,53873,53877,53880,53974,54030,54034,54040,54043,54050,54056,54149,54152,54156,54161,54178,54180,54185,54191,54194,54198,54222],[17,53536,53531],{"id":53537},"the-right-way-to-handle-hover-in-css-across-devices",[27,53539,53540],{},[30,53541,53542,36,53544,40,53546],{},[33,53543],{"value":35},[33,53545],{"value":39},[42,53547,53548],{},[45,53549,53550],{"href":47},[33,53551],{"value":50},[52,53553],{":tags":54},[56,53555],{":audio-src":53556,":transcript-src":53557},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F09\u002F21\u002Fthe-right-way-to-handle-hover-in-css-across-devices\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F09\u002F21\u002Fthe-right-way-to-handle-hover-in-css-across-devices\u002Fsummary.json",[27,53559,53560],{},[63,53561],{"alt":12847,"src":53562},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1758442342\u002Fblog\u002Fthe-right-way-to-handle-hover-in-css-across-devices\u002Fthe-right-way-to-handle-hover-in-css-across-devices_j4pxou",[27,53564,53565],{},"In today's diverse technological landscape, browsers run on a wide variety of devices, but hover functionality isn't universally supported across all of them. For example, mobile phones and tablets don't support hover functionality, so hover animations on buttons or cards don't make sense in CSS for these devices. In some cases, you might have devices that support hover when using a mouse but not when using touch screens.",[27,53567,53568],{},"In the past, we used different techniques to address this issue. We'll examine these common approaches that are still used even today. Later, we'll see how we can replace these less-than-ideal solutions with a neat feature that I personally wasn't aware existed and is well-supported in all browsers, I just learned about it recently.",[104,53570,53572],{"id":53571},"why-we-need-to-detect-hover-capable-devices","Why We Need to Detect Hover-Capable Devices",[1003,53574,53575,53581,53587],{},[1006,53576,53577,53580],{},[42,53578,53579],{},"Avoid broken experiences on touch devices.","\nSome hover effects (like dropdown menus or tooltips) don't work well on mobile. Using appropriate detection lets you provide alternative behaviour (like showing the menu on tap instead of hover).",[1006,53582,53583,53586],{},[42,53584,53585],{},"Improve accessibility & usability","\nMobile users don't have hover, so a hover-only interaction could confuse them. With proper detection, you can design for both input types.",[1006,53588,53589,53592],{},[42,53590,53591],{},"Performance optimisation","\nOn touch devices, you might not want heavy hover animations (like transitions that never get triggered). Using hover detection, you can remove them for better performance.",[104,53594,53596,53597,114,53600],{"id":53595},"using-css-media-queries-with-min-width-and-max-width","Using CSS media queries with ",[22,53598,53599],{},"min-width",[22,53601,53602],{},"max-width",[27,53604,53605],{},"Developers traditionally relied on screen size as a proxy for hover capability. Smaller screens were assumed to be touch-only devices without hover support, while larger ones were presumed to have hover functionality.",[128,53607,53609],{"className":23162,"code":53608,"language":23164,"meta":133,"style":133},"\u002F* Assume desktop supports hover *\u002F\n@media (min-width: 769px) {\n    .button:hover {\n        background-color: #2980b9;\n    }\n}\n\n\u002F* Assume mobile doesn't support hover *\u002F\n@media (max-width: 768px) {\n    .button:active {\n        background-color: #2980b9;\n    }\n}\n",[22,53610,53611,53616,53634,53641,53653,53657,53661,53665,53670,53687,53694,53704,53708],{"__ignoreMap":133},[137,53612,53613],{"class":139,"line":140},[137,53614,53615],{"class":308},"\u002F* Assume desktop supports hover *\u002F\n",[137,53617,53618,53621,53623,53625,53627,53630,53632],{"class":139,"line":173},[137,53619,53620],{"class":143},"@media",[137,53622,158],{"class":157},[137,53624,53599],{"class":364},[137,53626,726],{"class":157},[137,53628,53629],{"class":364},"769",[137,53631,39722],{"class":143},[137,53633,170],{"class":157},[137,53635,53636,53639],{"class":139,"line":188},[137,53637,53638],{"class":147},"    .button:hover",[137,53640,256],{"class":157},[137,53642,53643,53646,53648,53651],{"class":139,"line":269},[137,53644,53645],{"class":364},"        background-color",[137,53647,726],{"class":157},[137,53649,53650],{"class":364},"#2980b9",[137,53652,3276],{"class":157},[137,53654,53655],{"class":139,"line":278},[137,53656,294],{"class":157},[137,53658,53659],{"class":139,"line":291},[137,53660,510],{"class":157},[137,53662,53663],{"class":139,"line":297},[137,53664,516],{"emptyLinePlaceholder":515},[137,53666,53667],{"class":139,"line":302},[137,53668,53669],{"class":308},"\u002F* Assume mobile doesn't support hover *\u002F\n",[137,53671,53672,53674,53676,53678,53680,53683,53685],{"class":139,"line":662},[137,53673,53620],{"class":143},[137,53675,158],{"class":157},[137,53677,53602],{"class":364},[137,53679,726],{"class":157},[137,53681,53682],{"class":364},"768",[137,53684,39722],{"class":143},[137,53686,170],{"class":157},[137,53688,53689,53692],{"class":139,"line":667},[137,53690,53691],{"class":147},"    .button:active",[137,53693,256],{"class":157},[137,53695,53696,53698,53700,53702],{"class":139,"line":786},[137,53697,53645],{"class":364},[137,53699,726],{"class":157},[137,53701,53650],{"class":364},[137,53703,3276],{"class":157},[137,53705,53706],{"class":139,"line":798},[137,53707,294],{"class":157},[137,53709,53710],{"class":139,"line":803},[137,53711,510],{"class":157},[27,53713,53714],{},"However, this approach is unreliable. Screen size doesn't always correlate with input type, small laptops support hover, while large tablets typically don't. At best, it's an imprecise assumption.",[104,53716,53718,53719,14105],{"id":53717},"using-javascript-ontouchstart","Using JavaScript (",[22,53720,53721],{},"ontouchstart",[27,53723,53724],{},"Another approach was to check if the device supported touch events. If it did, developers assumed no hover.",[128,53726,53728],{"className":130,"code":53727,"language":132,"meta":133,"style":133},"const hasTouch = \"ontouchstart\" in window && navigator.maxTouchPoints > 0;\n\nif (hasTouch) {\n    document.body.classList.add(\"no-hover\");\n} else {\n    document.body.classList.add(\"hover\");\n}\n",[22,53729,53730,53758,53762,53769,53783,53791,53804],{"__ignoreMap":133},[137,53731,53732,53734,53737,53739,53742,53744,53747,53749,53752,53754,53756],{"class":139,"line":140},[137,53733,3077],{"class":143},[137,53735,53736],{"class":364}," hasTouch",[137,53738,151],{"class":143},[137,53740,53741],{"class":284}," \"ontouchstart\"",[137,53743,13022],{"class":143},[137,53745,53746],{"class":157}," window ",[137,53748,3351],{"class":143},[137,53750,53751],{"class":157}," navigator.maxTouchPoints ",[137,53753,30029],{"class":143},[137,53755,7687],{"class":364},[137,53757,3276],{"class":157},[137,53759,53760],{"class":139,"line":173},[137,53761,516],{"emptyLinePlaceholder":515},[137,53763,53764,53766],{"class":139,"line":188},[137,53765,3340],{"class":143},[137,53767,53768],{"class":157}," (hasTouch) {\n",[137,53770,53771,53774,53776,53778,53781],{"class":139,"line":269},[137,53772,53773],{"class":157},"    document.body.classList.",[137,53775,34393],{"class":147},[137,53777,356],{"class":157},[137,53779,53780],{"class":284},"\"no-hover\"",[137,53782,1502],{"class":157},[137,53784,53785,53787,53789],{"class":139,"line":278},[137,53786,50801],{"class":157},[137,53788,24947],{"class":143},[137,53790,256],{"class":157},[137,53792,53793,53795,53797,53799,53802],{"class":139,"line":291},[137,53794,53773],{"class":157},[137,53796,34393],{"class":147},[137,53798,356],{"class":157},[137,53800,53801],{"class":284},"\"hover\"",[137,53803,1502],{"class":157},[137,53805,53806],{"class":139,"line":297},[137,53807,510],{"class":157},[128,53809,53811],{"className":23162,"code":53810,"language":23164,"meta":133,"style":133},"body.hover .button:hover {\n    background-color: #2980b9;\n}\n\nbody.no-hover .button:active {\n    background-color: #2980b9;\n}\n",[22,53812,53813,53825,53836,53840,53844,53856,53866],{"__ignoreMap":133},[137,53814,53815,53817,53820,53823],{"class":139,"line":140},[137,53816,4065],{"class":4036},[137,53818,53819],{"class":147},".hover",[137,53821,53822],{"class":147}," .button:hover",[137,53824,256],{"class":157},[137,53826,53827,53830,53832,53834],{"class":139,"line":173},[137,53828,53829],{"class":364},"    background-color",[137,53831,726],{"class":157},[137,53833,53650],{"class":364},[137,53835,3276],{"class":157},[137,53837,53838],{"class":139,"line":188},[137,53839,510],{"class":157},[137,53841,53842],{"class":139,"line":269},[137,53843,516],{"emptyLinePlaceholder":515},[137,53845,53846,53848,53851,53854],{"class":139,"line":278},[137,53847,4065],{"class":4036},[137,53849,53850],{"class":147},".no-hover",[137,53852,53853],{"class":147}," .button:active",[137,53855,256],{"class":157},[137,53857,53858,53860,53862,53864],{"class":139,"line":291},[137,53859,53829],{"class":364},[137,53861,726],{"class":157},[137,53863,53650],{"class":364},[137,53865,3276],{"class":157},[137,53867,53868],{"class":139,"line":297},[137,53869,510],{"class":157},[27,53871,53872],{},"Hybrid devices (like laptops with touchscreens) break this logic. They support touch and hover, and forcing them into one category creates inconsistent behaviour.",[104,53874,53876],{"id":53875},"using-user-agent-sniffing","Using User-Agent Sniffing",[27,53878,53879],{},"A particularly fragile approach was examining the user agent string to determine whether a device was mobile or desktop.",[128,53881,53883],{"className":130,"code":53882,"language":132,"meta":133,"style":133},"const isTouchDevice = \u002FMobi|Android|iPhone|iPad\u002Fi.test(navigator.userAgent);\n\nif (isTouchDevice) {\n    document.body.classList.add(\"no-hover\");\n} else {\n    document.body.classList.add(\"hover\");\n}\n",[22,53884,53885,53927,53931,53938,53950,53958,53970],{"__ignoreMap":133},[137,53886,53887,53889,53892,53894,53896,53899,53901,53904,53906,53909,53911,53914,53916,53919,53921,53924],{"class":139,"line":140},[137,53888,3077],{"class":143},[137,53890,53891],{"class":364}," isTouchDevice",[137,53893,151],{"class":143},[137,53895,38406],{"class":284},[137,53897,53898],{"class":14746},"Mobi",[137,53900,7684],{"class":143},[137,53902,53903],{"class":14746},"Android",[137,53905,7684],{"class":143},[137,53907,53908],{"class":14746},"iPhone",[137,53910,7684],{"class":143},[137,53912,53913],{"class":14746},"iPad",[137,53915,47],{"class":284},[137,53917,53918],{"class":143},"i",[137,53920,1017],{"class":157},[137,53922,53923],{"class":147},"test",[137,53925,53926],{"class":157},"(navigator.userAgent);\n",[137,53928,53929],{"class":139,"line":173},[137,53930,516],{"emptyLinePlaceholder":515},[137,53932,53933,53935],{"class":139,"line":188},[137,53934,3340],{"class":143},[137,53936,53937],{"class":157}," (isTouchDevice) {\n",[137,53939,53940,53942,53944,53946,53948],{"class":139,"line":269},[137,53941,53773],{"class":157},[137,53943,34393],{"class":147},[137,53945,356],{"class":157},[137,53947,53780],{"class":284},[137,53949,1502],{"class":157},[137,53951,53952,53954,53956],{"class":139,"line":278},[137,53953,50801],{"class":157},[137,53955,24947],{"class":143},[137,53957,256],{"class":157},[137,53959,53960,53962,53964,53966,53968],{"class":139,"line":291},[137,53961,53773],{"class":157},[137,53963,34393],{"class":147},[137,53965,356],{"class":157},[137,53967,53801],{"class":284},[137,53969,1502],{"class":157},[137,53971,53972],{"class":139,"line":297},[137,53973,510],{"class":157},[128,53975,53976],{"className":23162,"code":53810,"language":23164,"meta":133,"style":133},[22,53977,53978,53988,53998,54002,54006,54016,54026],{"__ignoreMap":133},[137,53979,53980,53982,53984,53986],{"class":139,"line":140},[137,53981,4065],{"class":4036},[137,53983,53819],{"class":147},[137,53985,53822],{"class":147},[137,53987,256],{"class":157},[137,53989,53990,53992,53994,53996],{"class":139,"line":173},[137,53991,53829],{"class":364},[137,53993,726],{"class":157},[137,53995,53650],{"class":364},[137,53997,3276],{"class":157},[137,53999,54000],{"class":139,"line":188},[137,54001,510],{"class":157},[137,54003,54004],{"class":139,"line":269},[137,54005,516],{"emptyLinePlaceholder":515},[137,54007,54008,54010,54012,54014],{"class":139,"line":278},[137,54009,4065],{"class":4036},[137,54011,53850],{"class":147},[137,54013,53853],{"class":147},[137,54015,256],{"class":157},[137,54017,54018,54020,54022,54024],{"class":139,"line":291},[137,54019,53829],{"class":364},[137,54021,726],{"class":157},[137,54023,53650],{"class":364},[137,54025,3276],{"class":157},[137,54027,54028],{"class":139,"line":297},[137,54029,510],{"class":157},[104,54031,54033],{"id":54032},"why-these-solutions-felt-problematic","Why These Solutions Felt Problematic",[27,54035,54036,54037],{},"All three approaches: screen size, touch detection, and user-agent sniffing were valid attempts, but they all made assumptions instead of directly asking the device: ",[30,54038,54039],{},"“Can you hover?”",[27,54041,54042],{},"As a result, they often failed in edge cases like hybrid devices, unusual screen sizes, or browsers that changed behaviour over time.",[104,54044,54046,54047],{"id":54045},"the-right-solution-media-hover","The Right Solution: ",[22,54048,54049],{},"@media (hover)",[27,54051,54052,54053,54055],{},"Modern CSS gives us a direct, declarative way to handle hover capability with the ",[22,54054,54049],{}," feature. No guessing, no JavaScript, no hacks.",[128,54057,54059],{"className":23162,"code":54058,"language":23164,"meta":133,"style":133},"\u002F* Devices that support hover (e.g. mouse, trackpad) *\u002F\n@media (hover: hover) {\n    .button:hover {\n        background-color: #2980b9;\n    }\n}\n\n\u002F* Devices that don’t support hover (e.g. touchscreens) *\u002F\n@media (hover: none) {\n    .button:active {\n        background-color: #2980b9;\n    }\n}\n",[22,54060,54061,54066,54081,54087,54097,54101,54105,54109,54114,54125,54131,54141,54145],{"__ignoreMap":133},[137,54062,54063],{"class":139,"line":140},[137,54064,54065],{"class":308},"\u002F* Devices that support hover (e.g. mouse, trackpad) *\u002F\n",[137,54067,54068,54070,54072,54075,54077,54079],{"class":139,"line":173},[137,54069,53620],{"class":143},[137,54071,158],{"class":157},[137,54073,54074],{"class":364},"hover",[137,54076,726],{"class":157},[137,54078,54074],{"class":364},[137,54080,170],{"class":157},[137,54082,54083,54085],{"class":139,"line":188},[137,54084,53638],{"class":147},[137,54086,256],{"class":157},[137,54088,54089,54091,54093,54095],{"class":139,"line":269},[137,54090,53645],{"class":364},[137,54092,726],{"class":157},[137,54094,53650],{"class":364},[137,54096,3276],{"class":157},[137,54098,54099],{"class":139,"line":278},[137,54100,294],{"class":157},[137,54102,54103],{"class":139,"line":291},[137,54104,510],{"class":157},[137,54106,54107],{"class":139,"line":297},[137,54108,516],{"emptyLinePlaceholder":515},[137,54110,54111],{"class":139,"line":302},[137,54112,54113],{"class":308},"\u002F* Devices that don’t support hover (e.g. touchscreens) *\u002F\n",[137,54115,54116,54118,54120,54122],{"class":139,"line":662},[137,54117,53620],{"class":143},[137,54119,158],{"class":157},[137,54121,54074],{"class":364},[137,54123,54124],{"class":157},": none) {\n",[137,54126,54127,54129],{"class":139,"line":667},[137,54128,53691],{"class":147},[137,54130,256],{"class":157},[137,54132,54133,54135,54137,54139],{"class":139,"line":786},[137,54134,53645],{"class":364},[137,54136,726],{"class":157},[137,54138,53650],{"class":364},[137,54140,3276],{"class":157},[137,54142,54143],{"class":139,"line":798},[137,54144,294],{"class":157},[137,54146,54147],{"class":139,"line":803},[137,54148,510],{"class":157},[27,54150,54151],{},"This solution is cleaner, more reliable, and future-proof. It explicitly queries the device’s primary input mechanism, so you know exactly what you’re working with.",[104,54153,54155],{"id":54154},"browser-support","Browser Support",[27,54157,54158,54160],{},[22,54159,54049],{}," is widely supported across all modern browsers and we can safely use without worrying about compatibility.",[1003,54162,54163,54166,54169,54172,54175],{},[1006,54164,54165],{},"Chrome: 38+",[1006,54167,54168],{},"Firefox: 64+",[1006,54170,54171],{},"Safari: 9+",[1006,54173,54174],{},"Edge: 12+",[1006,54176,54177],{},"Opera: 25+",[104,54179,2567],{"id":2566},[27,54181,4737,54182,54184],{},[22,54183,54049],{}," feature query has been with us for a while now, even though many developers (myself included) aren't aware of it and continue using less reliable alternatives. This powerful CSS feature provides a direct way to detect hover capability without making assumptions based on screen size, touch support, or user agent strings.",[27,54186,54187,54188,54190],{},"By using ",[22,54189,54049],{},", we can create truly responsive designs that adapt to the user's input method rather than their device type.",[27,54192,54193],{},"The wide browser support makes it a practical solution we can implement today, helping us build more accessible, performant, and user-friendly interfaces across all devices.",[123,54195,54197],{"id":54196},"useful-resources","Useful Resources",[1003,54199,54200,54207,54214],{},[1006,54201,54202],{},[45,54203,54206],{"href":54204,"target":2716,"rel":54205},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002F@media\u002Fhover",[2718,2719],"MDN: @media (hover)",[1006,54208,54209],{},[45,54210,54213],{"href":54211,"target":2716,"rel":54212},"https:\u002F\u002Fwww.smashingmagazine.com\u002F2022\u002F03\u002Fguide-hover-pointer-media-queries\u002F",[2718,2719],"Smashing Magazine: A Guide To Hover And Pointer Media Queries",[1006,54215,54216,54217,54221],{},"The code of this examples can we find in the following ",[45,54218,50876],{"href":54219,"target":2716,"rel":54220},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fthe-right-way-to-handle-hover-in-css-across-devices",[2718,2719]," repo.",[2617,54223,54224],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":133,"searchDepth":173,"depth":173,"links":54226},[54227,54228,54230,54232,54233,54234,54236,54237],{"id":53571,"depth":173,"text":53572},{"id":53595,"depth":173,"text":54229},"Using CSS media queries with min-width and max-width",{"id":53717,"depth":173,"text":54231},"Using JavaScript (ontouchstart)",{"id":53875,"depth":173,"text":53876},{"id":54032,"depth":173,"text":54033},{"id":54045,"depth":173,"text":54235},"The Right Solution: @media (hover)",{"id":54154,"depth":173,"text":54155},{"id":2566,"depth":173,"text":2567,"children":54238},[54239],{"id":54196,"depth":188,"text":54197},"Learn the right way to handle hover effects in CSS across devices. Discover why methods like screen-size queries, touch detection, and user-agent sniffing fail, and how @media (hover) provides a cleaner, more reliable solution for responsive, accessible designs.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1758442342\u002Fblog\u002Fthe-right-way-to-handle-hover-in-css-across-devices\u002Fthe-right-way-to-handle-hover-in-css-across-devices_j4pxou",[54243,54244,54245,54246,54247,54248,54249,54250,54251,54252],"CSS hover","CSS media queries","detect hover devices","CSS @media hover","hover effects mobile","responsive CSS hover","touch vs hover","hover detection","cross-device CSS","CSS accessibility",{},"\u002F2025\u002F09\u002F21\u002Fthe-right-way-to-handle-hover-in-css-across-devices","21st September 2025",{"title":53531,"description":54240},"2025\u002F09\u002F21\u002Fthe-right-way-to-handle-hover-in-css-across-devices","E990WZtQmk281jmbQCwRqz-_wvKKgkMU3_1vsUrAgSM",{"id":54260,"title":54261,"articleTags":54262,"author":11,"blog":12,"body":54264,"description":54580,"extension":2649,"image":54581,"keywords":54582,"meta":54597,"navigation":515,"path":54598,"published":54599,"readTime":278,"seo":54600,"stem":54601,"type":2662,"__hash__":54602},"content\u002F2025\u002F10\u002F11\u002Fdecoupling-a-system-with-nestjs-microservices.md","Decoupling a System with Nest.js Microservices",[12816,2669,54263],"AWS",{"type":14,"value":54265,"toc":54564},[54266,54269,54283,54285,54289,54294,54297,54304,54308,54311,54317,54331,54338,54342,54345,54353,54367,54370,54376,54379,54386,54400,54408,54418,54422,54425,54454,54457,54461,54464,54490,54494,54505,54522,54525,54529,54532,54543,54547,54557],[17,54267,54261],{"id":54268},"decoupling-a-system-with-nestjs-microservices",[27,54270,54271],{},[30,54272,54273,36,54275,40,54277],{},[33,54274],{"value":35},[33,54276],{"value":39},[42,54278,54279],{},[45,54280,54281],{"href":47},[33,54282],{"value":50},[52,54284],{":tags":54},[56,54286],{":audio-src":54287,":transcript-src":54288},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F10\u002F11\u002Fdecoupling-a-system-with-nestjs-microservices\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F10\u002F11\u002Fdecoupling-a-system-with-nestjs-microservices\u002Fsummary.json",[27,54290,54291],{},[63,54292],{"alt":12847,"src":54293},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1760140132\u002Fblog\u002Fdecoupling-a-system-with-nestjs-microservices\u002Fdecoupling-a-system-with-nestjs-microservices_hvyd6p",[27,54295,54296],{},"Over the past few weeks, I've been experimenting with building a small microservice architecture using Nest.js, Amazon SQS, Amazon SES and DynamoDB. My goal was to create a decoupled system where three services work together through queues, one accepts orders, another saves them, and a third notifies the customer.",[27,54298,54299,54300],{},"In this article, I'll walk you through an overview of the project, how the services are structured, and why this setup makes sense. This is not a step-by-step tutorial, but rather a tour of the system. If you want to dive into the details, you can check out the full source code on ",[45,54301,50876],{"href":54302,"target":2716,"rel":54303},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Forder-microservices",[2718,2719],[104,54305,54307],{"id":54306},"the-monorepo-setup","The Monorepo Setup",[27,54309,54310],{},"I started with a Nest.js monorepo, which keeps everything in one place but neatly separated:",[128,54312,54315],{"className":54313,"code":54314,"language":5189},[5187],"order-microservices\u002F\n  apps\u002F\n    order-producer\u002F\n    order-persistence\u002F\n    order-notification\u002F\n  libs\u002F\n    aws-clients\u002F\n    common-dto\u002F\n    common-utils\u002F\n    sqs-microservice\u002F\n  nest-cli.json\n",[22,54316,54314],{"__ignoreMap":133},[1003,54318,54319,54325],{},[1006,54320,54321,54324],{},[42,54322,54323],{},"Apps"," → Each folder is a microservice.",[1006,54326,54327,54330],{},[42,54328,54329],{},"Libs"," → Shared libraries so we don't repeat ourselves (AWS clients, DTOs, utilities).",[27,54332,54333,54334,1017],{},"If you want to learn how to set up a Nest.js monorepo, check out my other post: ",[45,54335,54337],{"href":54336},"\u002F2025\u002F10\u002F12\u002Fhow-to-structure-a-nestjs-project-for-microservices-monorepo-setup","How to Structure a Nest.js Project for Microservices (Monorepo Setup)",[104,54339,54341],{"id":54340},"meet-the-services","Meet the Services",[27,54343,54344],{},"Think of each service as a specialised team member in a small company. Each one has a single responsibility and communicates with others by passing messages.",[123,54346,54348,54349,54352],{"id":54347},"_1-order-producer-the-front-door","1. ",[42,54350,54351],{},"order-producer"," – The Front Door",[1003,54354,54355,54361,54364],{},[1006,54356,54357,54358,4409],{},"A simple HTTP API (",[22,54359,54360],{},"POST \u002Forders",[1006,54362,54363],{},"It accepts an order and immediately publishes messages to SQS.",[1006,54365,54366],{},"Returns quickly to the client while the heavy lifting happens in the background.",[27,54368,54369],{},"Example request:",[128,54371,54374],{"className":54372,"code":54373,"language":5189},[5187],"POST http:\u002F\u002Flocalhost:3000\u002Forders\nContent-Type: application\u002Fjson\n\n{\n  \"orderId\": \"demo-1759035874968\",\n  \"customerId\": \"customer-42\",\n  \"totalAmount\": 129.99,\n  \"currency\": \"USD\",\n  \"items\": [\n    { \"sku\": \"sku-123\", \"title\": \"Wireless Headphones\", \"quantity\": 1, \"unitPrice\": 129.99 }\n  ],\n  \"notes\": \"REST Client sample order\",\n  \"metadata\": { \"channel\": \"rest-client\", \"requestedAt\": \"1759035874964\" }\n}\n",[22,54375,54373],{"__ignoreMap":133},[27,54377,54378],{},"Behind the scenes, this produces two messages: one for persistence and one for notification.",[123,54380,54382,54385],{"id":54381},"_3-order-persistence-the-record-keeper",[42,54383,54384],{},"3. order-persistence"," – The Record Keeper",[1003,54387,54388,54391,54394,54397],{},[1006,54389,54390],{},"No HTTP endpoints.",[1006,54392,54393],{},"Listens to its own SQS queue.",[1006,54395,54396],{},"Saves each incoming order message to DynamoDB.",[1006,54398,54399],{},"Uses SQS retry mechanism to ensure reliability even when failures occur.",[123,54401,54403,54404,54407],{"id":54402},"_3-order-notification-the-messenger","3. ",[42,54405,54406],{},"order-notification"," – The Messenger",[1003,54409,54410,54412,54415],{},[1006,54411,54390],{},[1006,54413,54414],{},"Listens to another SQS queue.",[1006,54416,54417],{},"When an order arrives, it sends an email via SES.",[104,54419,54421],{"id":54420},"shared-libraries-the-toolbox","Shared Libraries – The Toolbox",[27,54423,54424],{},"Instead of duplicating code, I extracted common logic into shared libraries:",[1003,54426,54427,54433,54439,54445],{},[1006,54428,54429,54432],{},[42,54430,54431],{},"aws-clients"," → Configures SQS, DynamoDB, SES clients (with LocalStack support for dev).",[1006,54434,54435,54438],{},[42,54436,54437],{},"common-dto"," → Defines TypeScript DTOs for orders so all services speak the same language.",[1006,54440,54441,54444],{},[42,54442,54443],{},"common-utils"," → Centralses small configuration helpers—like safe numeric environment parsing—and provides getSqsPollingSettings to standardize SQS polling configurations (batch size, wait time, visibility timeout, error backoff) across services.",[1006,54446,54447,54450,54451,10932],{},[42,54448,54449],{},"sqs-microservice"," → The \"special sauce.\" A custom Nest.js microservice transport for SQS that lets services register handlers with ",[22,54452,54453],{},"@MessagePattern",[27,54455,54456],{},"This keeps each microservice focused only on its business logic, not AWS plumbing.",[104,54458,54460],{"id":54459},"why-this-works","Why This Works",[27,54462,54463],{},"This structure gives us a few key benefits:",[1003,54465,54466,54472,54478,54484],{},[1006,54467,54468,54471],{},[42,54469,54470],{},"Single responsibility per service"," → Easier to understand and scale independently.",[1006,54473,54474,54477],{},[42,54475,54476],{},"Loose coupling with SQS"," → Producers don't wait for consumers, and failures don't block the system.",[1006,54479,54480,54483],{},[42,54481,54482],{},"Consistency with shared libraries"," → All AWS clients and DTOs are in one place.",[1006,54485,54486,54489],{},[42,54487,54488],{},"Clean monorepo"," → Everything lives together but remains clearly separated.",[104,54491,54493],{"id":54492},"local-development","Local Development",[27,54495,54496,54497,54500,54501,54504],{},"I wired everything to run locally using ",[42,54498,54499],{},"LocalStack"," (for SQS + DynamoDB) and a ",[42,54502,54503],{},"local SES inbox",". That means you can:",[1003,54506,54507,54513,54519],{},[1006,54508,54509,54510,4409],{},"Watch emails arrive in a browser (",[22,54511,54512],{},"http:\u002F\u002Flocalhost:8005",[1006,54514,54515,54516,4409],{},"Explore DynamoDB items in a web UI (",[22,54517,54518],{},"http:\u002F\u002Flocalhost:8001",[1006,54520,54521],{},"Send test orders with curl or VS Code's REST client.",[27,54523,54524],{},"In other words, it feels like production, but costs nothing.",[104,54526,54528],{"id":54527},"whats-next","What's Next?",[27,54530,54531],{},"This project is intentionally simple, just enough to show the building blocks of microservices with Nest.js and AWS. You can extend it in several ways:",[1003,54533,54534,54537,54540],{},[1006,54535,54536],{},"Add authentication to the producer.",[1006,54538,54539],{},"Create more notification channels.",[1006,54541,54542],{},"Scale workers by running multiple instances.",[104,54544,54546],{"id":54545},"final-notes","Final Notes",[27,54548,54549,54550,54553,54554,54556],{},"This post was an overview, not a comprehensive tutorial. If you'd like to explore the details, like the custom ",[22,54551,54552],{},"SqsServer"," class, ",[22,54555,54453],{}," handlers, and local setup scripts, you can check out the full code on GitHub:",[27,54558,54559,54560],{},"👉 ",[45,54561,54563],{"href":54302,"target":2716,"rel":54562},[2718,2719],"GitHub Repository Link",{"title":133,"searchDepth":173,"depth":173,"links":54565},[54566,54567,54575,54576,54577,54578,54579],{"id":54306,"depth":173,"text":54307},{"id":54340,"depth":173,"text":54341,"children":54568},[54569,54571,54573],{"id":54347,"depth":188,"text":54570},"1. order-producer – The Front Door",{"id":54381,"depth":188,"text":54572},"3. order-persistence – The Record Keeper",{"id":54402,"depth":188,"text":54574},"3. order-notification – The Messenger",{"id":54420,"depth":173,"text":54421},{"id":54459,"depth":173,"text":54460},{"id":54492,"depth":173,"text":54493},{"id":54527,"depth":173,"text":54528},{"id":54545,"depth":173,"text":54546},"Turn your Nest.js app into a distributed microservice system. Discover how message queues and shared libraries help create a loosely coupled backend. Each service handles its own responsibility, from order creation to persistence and notifications, without waiting on others. This approach makes your backend more resilient, fault-tolerant, and ready for real-world workloads.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1760140132\u002Fblog\u002Fdecoupling-a-system-with-nestjs-microservices\u002Fdecoupling-a-system-with-nestjs-microservices_hvyd6p",[54583,54584,54585,54586,54587,54588,54589,54590,54591,54592,54499,54593,54594,54595,54596],"Nest.js microservices","Nest.js monorepo","decoupled architecture","AWS SQS Nest.js","Amazon SES Nest.js","DynamoDB Nest.js","message queues","microservice communication","event-driven architecture","order processing system","Node.js backend","SQS message patterns","Nest.js project structure","scalable backend design",{},"\u002F2025\u002F10\u002F11\u002Fdecoupling-a-system-with-nestjs-microservices","11th October 2025",{"title":54261,"description":54580},"2025\u002F10\u002F11\u002Fdecoupling-a-system-with-nestjs-microservices","-TPhyOpLBYLcE0bocMP5yVGVYTvZdllhwQwsiYZ_K-U",{"id":54604,"title":54337,"articleTags":54605,"author":11,"blog":12,"body":54606,"description":55533,"extension":2649,"image":55534,"keywords":55535,"meta":55549,"navigation":515,"path":54336,"published":55550,"readTime":269,"seo":55551,"stem":55552,"type":2662,"__hash__":55553},"content\u002F2025\u002F10\u002F12\u002Fhow-to-structure-a-nestjs-project-for-microservices-monorepo-setup.md",[12816,2669,22224],{"type":14,"value":54607,"toc":55519},[54608,54611,54625,54627,54631,54636,54658,54661,54665,54680,54683,54706,54709,54713,54716,54761,54764,54770,54783,54795,54814,54818,54821,54824,54882,54885,54890,54897,54902,54905,54946,54949,54964,54967,54971,54974,54978,54983,55003,55006,55010,55024,55029,55150,55155,55264,55267,55273,55278,55449,55456,55460,55463,55507,55516],[17,54609,54337],{"id":54610},"how-to-structure-a-nestjs-project-for-microservices-monorepo-setup",[27,54612,54613],{},[30,54614,54615,36,54617,40,54619],{},[33,54616],{"value":35},[33,54618],{"value":39},[42,54620,54621],{},[45,54622,54623],{"href":47},[33,54624],{"value":50},[52,54626],{":tags":54},[56,54628],{":audio-src":54629,":transcript-src":54630},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F10\u002F12\u002Fhow-to-structure-a-nestjs-project-for-microservices-monorepo-setup\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F10\u002F12\u002Fhow-to-structure-a-nestjs-project-for-microservices-monorepo-setup\u002Fsummary.json",[27,54632,54633],{},[63,54634],{"alt":12847,"src":54635},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1760240493\u002Fblog\u002Fhow-to-structure-a-nestjs-project-for-microservices-monorepo-setup\u002Fhow-to-structure-a-nestjs-project-for-microservices-monorepo-setup_hfc2gz",[27,54637,54638,54639,164,54641,14528,54644,54646,54647,54650,54651,54654,54655,1017],{},"In this post, we'll walk through setting up a simple microservices project using Nest.js. Our goal is to create three separate services: ",[42,54640,54351],{},[42,54642,54643],{},"order-persistence",[42,54645,54406],{},", that communicate via ",[42,54648,54649],{},"Amazon SQS",", store data in ",[42,54652,54653],{},"DynamoDB",", and send notifications to customers using ",[42,54656,54657],{},"Amazon SES",[27,54659,54660],{},"This is just a setup guide, not a full tutorial on microservices. By the end, you’ll have a clean monorepo structure ready to extend. If you’d like to see the full working example, you can check out the GitHub repository.",[104,54662,54664],{"id":54663},"step-1-create-a-new-nestjs-project","Step 1: Create a New Nest.js Project",[27,54666,54667,54668,54670,54671,54673,54674,54676,54677,1017],{},"We'll use ",[22,54669,21167],{}," with the Nest.js CLI, but you can also install it globally as a dependency and use the ",[22,54672,12951],{}," command instead of ",[22,54675,21167],{},". For more details, check the official documentation ",[45,54678,10647],{"href":12918,"target":2716,"rel":54679},[2718,2719],[27,54681,54682],{},"First, scaffold a new Nest.js project",[128,54684,54686],{"className":8665,"code":54685,"language":8667,"meta":133,"style":133},"npx @nestjs\u002Fcli new order-microservices\ncd order-microservices\n",[22,54687,54688,54700],{"__ignoreMap":133},[137,54689,54690,54692,54695,54697],{"class":139,"line":140},[137,54691,21167],{"class":147},[137,54693,54694],{"class":284}," @nestjs\u002Fcli",[137,54696,1426],{"class":284},[137,54698,54699],{"class":284}," order-microservices\n",[137,54701,54702,54704],{"class":139,"line":173},[137,54703,9558],{"class":364},[137,54705,54699],{"class":284},[27,54707,54708],{},"This will generate the base standard Nest.js project. While this is a starting point, we'd like to convert our project to a monorepo where each app will act as a separate microservice.",[104,54710,54712],{"id":54711},"step-2-generate-the-microservices-monorepo-apps","Step 2: Generate the Microservices (Monorepo Apps)",[27,54714,54715],{},"Inside the Nest.js project we just created, let's create three separate apps:",[128,54717,54719],{"className":8665,"code":54718,"language":8667,"meta":133,"style":133},"npx @nestjs\u002Fcli generate app order-producer\nnpx @nestjs\u002Fcli generate app order-persistence\nnpx @nestjs\u002Fcli generate app order-notification\n",[22,54720,54721,54735,54748],{"__ignoreMap":133},[137,54722,54723,54725,54727,54730,54732],{"class":139,"line":140},[137,54724,21167],{"class":147},[137,54726,54694],{"class":284},[137,54728,54729],{"class":284}," generate",[137,54731,15064],{"class":284},[137,54733,54734],{"class":284}," order-producer\n",[137,54736,54737,54739,54741,54743,54745],{"class":139,"line":173},[137,54738,21167],{"class":147},[137,54740,54694],{"class":284},[137,54742,54729],{"class":284},[137,54744,15064],{"class":284},[137,54746,54747],{"class":284}," order-persistence\n",[137,54749,54750,54752,54754,54756,54758],{"class":139,"line":188},[137,54751,21167],{"class":147},[137,54753,54694],{"class":284},[137,54755,54729],{"class":284},[137,54757,15064],{"class":284},[137,54759,54760],{"class":284}," order-notification\n",[27,54762,54763],{},"Now your project structure looks like this:",[128,54765,54768],{"className":54766,"code":54767,"language":5189},[5187],"order-microservices\u002F\n  apps\u002F\n    order-microservices\u002F   👈 created by default\n    order-producer\u002F\n    order-persistence\u002F\n    order-notification\u002F\n  libs\u002F\n  nest-cli.json\n",[22,54769,54767],{"__ignoreMap":133},[3244,54771,54772],{},[27,54773,54774,54775,54778,54779,54782],{},"Note: The default ",[42,54776,54777],{},"order-microservices"," app has been moved inside ",[22,54780,54781],{},"apps\u002F"," along with the other apps we just created. We don't need it anymore since we've converted this to a monorepo.",[27,54784,54785,54786,54788,54789,164,54791,14528,54793,1017],{},"Let's remove ",[42,54787,54777],{}," and keep only ",[42,54790,54351],{},[42,54792,54643],{},[42,54794,54406],{},[128,54796,54798],{"className":8665,"code":54797,"language":8667,"meta":133,"style":133},"sudo rm -rf apps\u002Forder-microservices\n",[22,54799,54800],{"__ignoreMap":133},[137,54801,54802,54805,54808,54811],{"class":139,"line":140},[137,54803,54804],{"class":147},"sudo",[137,54806,54807],{"class":284}," rm",[137,54809,54810],{"class":364}," -rf",[137,54812,54813],{"class":284}," apps\u002Forder-microservices\n",[104,54815,54817],{"id":54816},"step-3-create-shared-libraries","Step 3: Create Shared Libraries",[27,54819,54820],{},"Microservices often need to share code such as: DTOs, utilities, AWS client configuration, and more. The Nest.js CLI makes this easy to set up and manage.",[27,54822,54823],{},"We'll use the library command from the Nest.js CLI to generate some shared libraries:",[128,54825,54827],{"className":8665,"code":54826,"language":8667,"meta":133,"style":133},"npx @nestjs\u002Fcli generate library common-dto\nnpx @nestjs\u002Fcli generate library aws-clients\nnpx @nestjs\u002Fcli generate library common-utils\nnpx @nestjs\u002Fcli generate library sqs-microservice\n",[22,54828,54829,54843,54856,54869],{"__ignoreMap":133},[137,54830,54831,54833,54835,54837,54840],{"class":139,"line":140},[137,54832,21167],{"class":147},[137,54834,54694],{"class":284},[137,54836,54729],{"class":284},[137,54838,54839],{"class":284}," library",[137,54841,54842],{"class":284}," common-dto\n",[137,54844,54845,54847,54849,54851,54853],{"class":139,"line":173},[137,54846,21167],{"class":147},[137,54848,54694],{"class":284},[137,54850,54729],{"class":284},[137,54852,54839],{"class":284},[137,54854,54855],{"class":284}," aws-clients\n",[137,54857,54858,54860,54862,54864,54866],{"class":139,"line":188},[137,54859,21167],{"class":147},[137,54861,54694],{"class":284},[137,54863,54729],{"class":284},[137,54865,54839],{"class":284},[137,54867,54868],{"class":284}," common-utils\n",[137,54870,54871,54873,54875,54877,54879],{"class":139,"line":269},[137,54872,21167],{"class":147},[137,54874,54694],{"class":284},[137,54876,54729],{"class":284},[137,54878,54839],{"class":284},[137,54880,54881],{"class":284}," sqs-microservice\n",[27,54883,54884],{},"Now your structure is much cleaner:",[128,54886,54888],{"className":54887,"code":54314,"language":5189},[5187],[22,54889,54314],{"__ignoreMap":133},[104,54891,54893,54894,14105],{"id":54892},"step-4-update-the-monorepo-config-nest-clijson","Step 4: Update the Monorepo Config (",[22,54895,54896],{},"nest-cli.json",[27,54898,54899,54900,1017],{},"Since we deleted the default app, we need to update ",[22,54901,54896],{},[27,54903,54904],{},"Remove the extra entries and keep only the structure pointing to our three apps. Delete the following lines:",[128,54906,54908],{"className":5155,"code":54907,"language":5157,"meta":133,"style":133},"\"sourceRoot\": \"apps\u002Forder-microservices\u002Fsrc\",\n\"tsConfigPath\": \"apps\u002Forder-microservices\u002Ftsconfig.app.json\",\n\"order-microservices\": { ... }\n",[22,54909,54910,54922,54934],{"__ignoreMap":133},[137,54911,54912,54915,54917,54920],{"class":139,"line":140},[137,54913,54914],{"class":284},"\"sourceRoot\"",[137,54916,726],{"class":157},[137,54918,54919],{"class":284},"\"apps\u002Forder-microservices\u002Fsrc\"",[137,54921,1961],{"class":157},[137,54923,54924,54927,54929,54932],{"class":139,"line":173},[137,54925,54926],{"class":284},"\"tsConfigPath\"",[137,54928,726],{"class":157},[137,54930,54931],{"class":284},"\"apps\u002Forder-microservices\u002Ftsconfig.app.json\"",[137,54933,1961],{"class":157},[137,54935,54936,54939,54942,54944],{"class":139,"line":188},[137,54937,54938],{"class":284},"\"order-microservices\"",[137,54940,54941],{"class":157},": { ",[137,54943,14408],{"class":8180},[137,54945,1115],{"class":157},[27,54947,54948],{},"And make sure the root looks like this:",[128,54950,54952],{"className":5155,"code":54951,"language":5157,"meta":133,"style":133},"\"root\": \"apps\"\n",[22,54953,54954],{"__ignoreMap":133},[137,54955,54956,54959,54961],{"class":139,"line":140},[137,54957,54958],{"class":284},"\"root\"",[137,54960,726],{"class":157},[137,54962,54963],{"class":284},"\"apps\"\n",[27,54965,54966],{},"This ensures Nest knows where to find your apps.",[104,54968,54970],{"id":54969},"step-5-configure-each-services-entry-point","Step 5: Configure Each Service’s Entry Point",[27,54972,54973],{},"Each service needs its own startup logic.",[123,54975,54977],{"id":54976},"producer-http-api-service","Producer (HTTP API service)",[27,54979,54980],{},[22,54981,54982],{},"apps\u002Forder-producer\u002Fsrc\u002Fmain.ts",[128,54984,54986],{"className":13299,"code":54985,"language":13301,"meta":133,"style":133},"await app.listen(3001);\n",[22,54987,54988],{"__ignoreMap":133},[137,54989,54990,54993,54995,54997,54999,55001],{"class":139,"line":140},[137,54991,54992],{"class":143},"await",[137,54994,15103],{"class":157},[137,54996,15106],{"class":147},[137,54998,356],{"class":157},[137,55000,22915],{"class":364},[137,55002,1502],{"class":157},[27,55004,55005],{},"This service exposes an HTTP endpoint and needs a port.",[123,55007,55009],{"id":55008},"persistence-notification-background-workers","Persistence & Notification (background workers)",[27,55011,55012,55013,26741,55016,55019,55020,1017],{},"Since these services only listen for messages from SQS, they don't need an HTTP port. Replace ",[22,55014,55015],{},"app.listen()",[22,55017,55018],{},"app.init()"," and convert them into microservices following the official Nest.js Microservices documentation ",[45,55021,10647],{"href":55022,"target":2716,"rel":55023},"https:\u002F\u002Fdocs.nestjs.com\u002Fmicroservices\u002Fbasics",[2718,2719],[27,55025,55026],{},[22,55027,55028],{},"apps\u002Forder-persistence\u002Fsrc\u002Fmain.ts",[128,55030,55032],{"className":13299,"code":55031,"language":13301,"meta":133,"style":133},"import { NestFactory } from \"@nestjs\u002Fcore\";\nimport { Transport, MicroserviceOptions } from \"@nestjs\u002Fmicroservices\";\nimport { AppModule } from \".\u002Fapp.module\";\n\nasync function bootstrap() {\n    const app = await NestFactory.createMicroservice\u003CMicroserviceOptions>(AppModule, {\n        transport: Transport.SQS, \u002F\u002F later replaced with custom SQS server\n    });\n    await app.init();\n}\nvoid bootstrap();\n",[22,55033,55034,55046,55060,55072,55076,55086,55109,55122,55126,55137,55141],{"__ignoreMap":133},[137,55035,55036,55038,55040,55042,55044],{"class":139,"line":140},[137,55037,10287],{"class":143},[137,55039,15008],{"class":157},[137,55041,10954],{"class":143},[137,55043,13917],{"class":284},[137,55045,3276],{"class":157},[137,55047,55048,55050,55053,55055,55058],{"class":139,"line":173},[137,55049,10287],{"class":143},[137,55051,55052],{"class":157}," { Transport, MicroserviceOptions } ",[137,55054,10954],{"class":143},[137,55056,55057],{"class":284}," \"@nestjs\u002Fmicroservices\"",[137,55059,3276],{"class":157},[137,55061,55062,55064,55066,55068,55070],{"class":139,"line":188},[137,55063,10287],{"class":143},[137,55065,15021],{"class":157},[137,55067,10954],{"class":143},[137,55069,15026],{"class":284},[137,55071,3276],{"class":157},[137,55073,55074],{"class":139,"line":269},[137,55075,516],{"emptyLinePlaceholder":515},[137,55077,55078,55080,55082,55084],{"class":139,"line":278},[137,55079,15050],{"class":143},[137,55081,154],{"class":143},[137,55083,15055],{"class":147},[137,55085,275],{"class":157},[137,55087,55088,55090,55092,55094,55096,55098,55101,55103,55106],{"class":139,"line":291},[137,55089,4177],{"class":143},[137,55091,15064],{"class":364},[137,55093,151],{"class":143},[137,55095,15069],{"class":143},[137,55097,15072],{"class":157},[137,55099,55100],{"class":147},"createMicroservice",[137,55102,4033],{"class":157},[137,55104,55105],{"class":147},"MicroserviceOptions",[137,55107,55108],{"class":157},">(AppModule, {\n",[137,55110,55111,55114,55117,55119],{"class":139,"line":297},[137,55112,55113],{"class":157},"        transport: Transport.",[137,55115,55116],{"class":364},"SQS",[137,55118,164],{"class":157},[137,55120,55121],{"class":308},"\u002F\u002F later replaced with custom SQS server\n",[137,55123,55124],{"class":139,"line":302},[137,55125,2832],{"class":157},[137,55127,55128,55130,55132,55135],{"class":139,"line":662},[137,55129,15100],{"class":143},[137,55131,15103],{"class":157},[137,55133,55134],{"class":147},"init",[137,55136,924],{"class":157},[137,55138,55139],{"class":139,"line":667},[137,55140,510],{"class":157},[137,55142,55143,55146,55148],{"class":139,"line":786},[137,55144,55145],{"class":143},"void",[137,55147,15055],{"class":147},[137,55149,924],{"class":157},[27,55151,55152],{},[22,55153,55154],{},"apps\u002Forder-notification\u002Fsrc\u002Fmain.ts",[128,55156,55158],{"className":13299,"code":55157,"language":13301,"meta":133,"style":133},"import { NestFactory } from \"@nestjs\u002Fcore\";\nimport { Transport, MicroserviceOptions } from \"@nestjs\u002Fmicroservices\";\nimport { AppModule } from \".\u002Fapp.module\";\n\nasync function bootstrap() {\n    const app = await NestFactory.createMicroservice\u003CMicroserviceOptions>(AppModule, {\n        transport: Transport.SQS,\n    });\n    await app.init();\n}\nvoid bootstrap();\n",[22,55159,55160,55172,55184,55196,55200,55210,55230,55238,55242,55252,55256],{"__ignoreMap":133},[137,55161,55162,55164,55166,55168,55170],{"class":139,"line":140},[137,55163,10287],{"class":143},[137,55165,15008],{"class":157},[137,55167,10954],{"class":143},[137,55169,13917],{"class":284},[137,55171,3276],{"class":157},[137,55173,55174,55176,55178,55180,55182],{"class":139,"line":173},[137,55175,10287],{"class":143},[137,55177,55052],{"class":157},[137,55179,10954],{"class":143},[137,55181,55057],{"class":284},[137,55183,3276],{"class":157},[137,55185,55186,55188,55190,55192,55194],{"class":139,"line":188},[137,55187,10287],{"class":143},[137,55189,15021],{"class":157},[137,55191,10954],{"class":143},[137,55193,15026],{"class":284},[137,55195,3276],{"class":157},[137,55197,55198],{"class":139,"line":269},[137,55199,516],{"emptyLinePlaceholder":515},[137,55201,55202,55204,55206,55208],{"class":139,"line":278},[137,55203,15050],{"class":143},[137,55205,154],{"class":143},[137,55207,15055],{"class":147},[137,55209,275],{"class":157},[137,55211,55212,55214,55216,55218,55220,55222,55224,55226,55228],{"class":139,"line":291},[137,55213,4177],{"class":143},[137,55215,15064],{"class":364},[137,55217,151],{"class":143},[137,55219,15069],{"class":143},[137,55221,15072],{"class":157},[137,55223,55100],{"class":147},[137,55225,4033],{"class":157},[137,55227,55105],{"class":147},[137,55229,55108],{"class":157},[137,55231,55232,55234,55236],{"class":139,"line":297},[137,55233,55113],{"class":157},[137,55235,55116],{"class":364},[137,55237,1961],{"class":157},[137,55239,55240],{"class":139,"line":302},[137,55241,2832],{"class":157},[137,55243,55244,55246,55248,55250],{"class":139,"line":662},[137,55245,15100],{"class":143},[137,55247,15103],{"class":157},[137,55249,55134],{"class":147},[137,55251,924],{"class":157},[137,55253,55254],{"class":139,"line":667},[137,55255,510],{"class":157},[137,55257,55258,55260,55262],{"class":139,"line":786},[137,55259,55145],{"class":143},[137,55261,15055],{"class":147},[137,55263,924],{"class":157},[55265,55266],"hr",{},[104,55268,55270,55271],{"id":55269},"step-6-add-scripts-to-packagejson","Step 6: Add Scripts to ",[22,55272,5140],{},[27,55274,55275,55276,894],{},"Finally, let’s make it easy to build and run each service separately by updating ",[22,55277,5140],{},[128,55279,55281],{"className":5155,"code":55280,"language":5157,"meta":133,"style":133},"\"build\": \"npm run build:producer && npm run build:persistence && npm run build:notification\",\n\"build:producer\": \"nest build order-producer\",\n\"build:persistence\": \"nest build order-persistence\",\n\"build:notification\": \"nest build order-notification\",\n\n\"start:producer\": \"nest start order-producer\",\n\"start:producer:dev\": \"nest start order-producer --watch\",\n\"start:producer:prod\": \"node dist\u002Fapps\u002Forder-producer\u002Fmain\",\n\n\"start:persistence\": \"nest start order-persistence\",\n\"start:persistence:dev\": \"nest start order-persistence --watch\",\n\"start:persistence:prod\": \"node dist\u002Fapps\u002Forder-persistence\u002Fmain\",\n\n\"start:notification\": \"nest start order-notification\",\n\"start:notification:dev\": \"nest start order-notification --watch\",\n\"start:notification:prod\": \"node dist\u002Fapps\u002Forder-notification\u002Fmain\"\n",[22,55282,55283,55295,55307,55319,55331,55335,55347,55359,55371,55375,55387,55399,55411,55415,55427,55439],{"__ignoreMap":133},[137,55284,55285,55288,55290,55293],{"class":139,"line":140},[137,55286,55287],{"class":284},"\"build\"",[137,55289,726],{"class":157},[137,55291,55292],{"class":284},"\"npm run build:producer && npm run build:persistence && npm run build:notification\"",[137,55294,1961],{"class":157},[137,55296,55297,55300,55302,55305],{"class":139,"line":173},[137,55298,55299],{"class":284},"\"build:producer\"",[137,55301,726],{"class":157},[137,55303,55304],{"class":284},"\"nest build order-producer\"",[137,55306,1961],{"class":157},[137,55308,55309,55312,55314,55317],{"class":139,"line":188},[137,55310,55311],{"class":284},"\"build:persistence\"",[137,55313,726],{"class":157},[137,55315,55316],{"class":284},"\"nest build order-persistence\"",[137,55318,1961],{"class":157},[137,55320,55321,55324,55326,55329],{"class":139,"line":269},[137,55322,55323],{"class":284},"\"build:notification\"",[137,55325,726],{"class":157},[137,55327,55328],{"class":284},"\"nest build order-notification\"",[137,55330,1961],{"class":157},[137,55332,55333],{"class":139,"line":278},[137,55334,516],{"emptyLinePlaceholder":515},[137,55336,55337,55340,55342,55345],{"class":139,"line":291},[137,55338,55339],{"class":284},"\"start:producer\"",[137,55341,726],{"class":157},[137,55343,55344],{"class":284},"\"nest start order-producer\"",[137,55346,1961],{"class":157},[137,55348,55349,55352,55354,55357],{"class":139,"line":297},[137,55350,55351],{"class":284},"\"start:producer:dev\"",[137,55353,726],{"class":157},[137,55355,55356],{"class":284},"\"nest start order-producer --watch\"",[137,55358,1961],{"class":157},[137,55360,55361,55364,55366,55369],{"class":139,"line":302},[137,55362,55363],{"class":284},"\"start:producer:prod\"",[137,55365,726],{"class":157},[137,55367,55368],{"class":284},"\"node dist\u002Fapps\u002Forder-producer\u002Fmain\"",[137,55370,1961],{"class":157},[137,55372,55373],{"class":139,"line":662},[137,55374,516],{"emptyLinePlaceholder":515},[137,55376,55377,55380,55382,55385],{"class":139,"line":667},[137,55378,55379],{"class":284},"\"start:persistence\"",[137,55381,726],{"class":157},[137,55383,55384],{"class":284},"\"nest start order-persistence\"",[137,55386,1961],{"class":157},[137,55388,55389,55392,55394,55397],{"class":139,"line":786},[137,55390,55391],{"class":284},"\"start:persistence:dev\"",[137,55393,726],{"class":157},[137,55395,55396],{"class":284},"\"nest start order-persistence --watch\"",[137,55398,1961],{"class":157},[137,55400,55401,55404,55406,55409],{"class":139,"line":798},[137,55402,55403],{"class":284},"\"start:persistence:prod\"",[137,55405,726],{"class":157},[137,55407,55408],{"class":284},"\"node dist\u002Fapps\u002Forder-persistence\u002Fmain\"",[137,55410,1961],{"class":157},[137,55412,55413],{"class":139,"line":803},[137,55414,516],{"emptyLinePlaceholder":515},[137,55416,55417,55420,55422,55425],{"class":139,"line":931},[137,55418,55419],{"class":284},"\"start:notification\"",[137,55421,726],{"class":157},[137,55423,55424],{"class":284},"\"nest start order-notification\"",[137,55426,1961],{"class":157},[137,55428,55429,55432,55434,55437],{"class":139,"line":1568},[137,55430,55431],{"class":284},"\"start:notification:dev\"",[137,55433,726],{"class":157},[137,55435,55436],{"class":284},"\"nest start order-notification --watch\"",[137,55438,1961],{"class":157},[137,55440,55441,55444,55446],{"class":139,"line":1573},[137,55442,55443],{"class":284},"\"start:notification:prod\"",[137,55445,726],{"class":157},[137,55447,55448],{"class":284},"\"node dist\u002Fapps\u002Forder-notification\u002Fmain\"\n",[27,55450,55451,55452,55455],{},"Now you can run each service independently, either in dev (",[22,55453,55454],{},"--watch",") or in prod mode.",[104,55457,55459],{"id":55458},"recap","Recap",[27,55461,55462],{},"At this point, you’ve:",[2569,55464,55465,55468,55480,55483,55495,55501,55504],{},[1006,55466,55467],{},"Created a Nest.js monorepo.",[1006,55469,55470,55471,164,55474,164,55477,4409],{},"Generated three microservices (",[22,55472,55473],{},"producer",[22,55475,55476],{},"persistence",[22,55478,55479],{},"notification",[1006,55481,55482],{},"Cleaned up the default app.",[1006,55484,55485,55486,164,55489,164,55491,164,55493,4409],{},"Added shared libraries (",[22,55487,55488],{},"dto",[22,55490,31956],{},[22,55492,54431],{},[22,55494,54449],{},[1006,55496,55497,55498,55500],{},"Updated ",[22,55499,54896],{}," to reflect the monorepo setup.",[1006,55502,55503],{},"Configured startup logic for each service.",[1006,55505,55506],{},"Added scripts to build and run services individually.",[27,55508,55509,55510,55513,55514,1017],{},"👉 If you want to see the ",[42,55511,55512],{},"next steps"," (including how the services actually talk to each other), check out the main blog post: ",[47718,55515,54261],{"to":54598},[2617,55517,55518],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":133,"searchDepth":173,"depth":173,"links":55520},[55521,55522,55523,55524,55526,55530,55532],{"id":54663,"depth":173,"text":54664},{"id":54711,"depth":173,"text":54712},{"id":54816,"depth":173,"text":54817},{"id":54892,"depth":173,"text":55525},"Step 4: Update the Monorepo Config (nest-cli.json)",{"id":54969,"depth":173,"text":54970,"children":55527},[55528,55529],{"id":54976,"depth":188,"text":54977},{"id":55008,"depth":188,"text":55009},{"id":55269,"depth":173,"text":55531},"Step 6: Add Scripts to package.json",{"id":55458,"depth":173,"text":55459},"A clean and scalable way to structure your Nest.js project for microservices. This step-by-step guide helps you organise multiple apps and shared libraries for a scalable architecture. Perfect for developers who want a maintainable, modular, and production-ready backend foundation with Nest.js.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1760240493\u002Fblog\u002Fhow-to-structure-a-nestjs-project-for-microservices-monorepo-setup\u002Fhow-to-structure-a-nestjs-project-for-microservices-monorepo-setup_hfc2gz",[54583,55536,54595,55537,55538,55539,55540,55541,55542,55543,55544,55545,55546,55547,55548],"Nest.js monorepo setup","Nest.js tutorial","Nest.js AWS SQS","Nest.js DynamoDB","Nest.js Amazon SES","Nest.js shared libraries","Nest.js microservice architecture","Nest.js CLI","Node.js microservices","Monorepo architecture Nest.js","Nest.js apps and libs structure","How to create microservices with Nest.js","Nest.js best practices",{},"12th October 2025",{"title":54337,"description":55533},"2025\u002F10\u002F12\u002Fhow-to-structure-a-nestjs-project-for-microservices-monorepo-setup","448ZeySd_RVNVZrXIZkINWABROPy-R8etLsswM5GasQ",{"id":55555,"title":55556,"articleTags":55557,"author":11,"blog":12,"body":55558,"description":56672,"extension":2649,"image":56673,"keywords":56674,"meta":56693,"navigation":515,"path":56694,"published":56695,"readTime":188,"seo":56696,"stem":56697,"type":2662,"__hash__":56698},"content\u002F2025\u002F11\u002F04\u002Fwhy-you-shouldnt-use-localstorage-for-transactions-and-what-to-use-instead.md","Why You Shouldn’t Use localStorage for Transactions - and What to Use Instead",[9,10,22707],{"type":14,"value":55559,"toc":56660},[55560,55563,55577,55579,55583,55588,55607,55610,55617,55622,55668,55671,55676,55691,55695,55698,55811,55821,55828,55843,55846,56204,56211,56218,56235,56249,56427,56436,56443,56452,56521,56535,56547,56554,56558,56561,56649,56657],[17,55561,55556],{"id":55562},"why-you-shouldnt-use-localstorage-for-transactions-and-what-to-use-instead",[27,55564,55565],{},[30,55566,55567,36,55569,40,55571],{},[33,55568],{"value":35},[33,55570],{"value":39},[42,55572,55573],{},[45,55574,55575],{"href":47},[33,55576],{"value":50},[52,55578],{":tags":54},[56,55580],{":audio-src":55581,":transcript-src":55582},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F11\u002F04\u002Fwhy-you-shouldnt-use-localstorage-for-transactions-and-what-to-use-instead\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F11\u002F04\u002Fwhy-you-shouldnt-use-localstorage-for-transactions-and-what-to-use-instead\u002Fsummary.json",[27,55584,55585],{},[63,55586],{"alt":12847,"src":55587},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1762215611\u002Fblog\u002Fwhy-you-shouldnt-use-localstorage-for-transactions-and-what-to-use-instead\u002Fwhy-you-shouldnt-use-localstorage-for-transactions-and-what-to-use-instead_kqnbsb",[27,55589,55590,55591,55594,55595,55597,55598,164,55601,14528,55604,1017],{},"When it comes to storing data in the browser, many of us reach for ",[22,55592,55593],{},"localStorage"," out of habit. It's simple, it works, and it's supported everywhere. But here's the thing - under the hood, ",[22,55596,55593],{}," has some serious limitations, especially when you're dealing with ",[30,55599,55600],{},"transactions",[30,55602,55603],{},"concurrent updates",[30,55605,55606],{},"performance-heavy tasks",[27,55608,55609],{},"Let's explore what those limitations are - and what better alternatives exist for building reliable, performant browser storage.",[104,55611,55613,55614,55616],{"id":55612},"how-localstorage-works","How ",[22,55615,55593],{}," Works",[27,55618,55619,55621],{},[22,55620,55593],{}," is a simple key-value storage system that's built into every modern browser. You've probably used it before - it looks something like this:",[128,55623,55625],{"className":130,"code":55624,"language":132,"meta":133,"style":133},"localStorage.setItem(\"theme\", \"dark\");\nconst theme = localStorage.getItem(\"theme\");\n",[22,55626,55627,55647],{"__ignoreMap":133},[137,55628,55629,55632,55635,55637,55640,55642,55645],{"class":139,"line":140},[137,55630,55631],{"class":157},"localStorage.",[137,55633,55634],{"class":147},"setItem",[137,55636,356],{"class":157},[137,55638,55639],{"class":284},"\"theme\"",[137,55641,164],{"class":157},[137,55643,55644],{"class":284},"\"dark\"",[137,55646,1502],{"class":157},[137,55648,55649,55651,55654,55656,55659,55662,55664,55666],{"class":139,"line":173},[137,55650,3077],{"class":143},[137,55652,55653],{"class":364}," theme",[137,55655,151],{"class":143},[137,55657,55658],{"class":157}," localStorage.",[137,55660,55661],{"class":147},"getItem",[137,55663,356],{"class":157},[137,55665,55639],{"class":284},[137,55667,1502],{"class":157},[27,55669,55670],{},"Pretty straightforward, right? But here's the catch:",[3244,55672,55673],{},[27,55674,55675],{},"🕒 localStorage operations are synchronous.",[27,55677,55678,55679,164,55681,29088,55683,55686,55687,55690],{},"That means every ",[22,55680,55634],{},[22,55682,55661],{},[22,55684,55685],{},"removeItem"," call blocks the ",[42,55688,55689],{},"main thread"," until it's done. In small doses, no big deal. But when you start doing large or repeated operations, it can actually freeze your UI and create a poor experience.",[104,55692,55694],{"id":55693},"example-blocking-the-main-thread","Example: Blocking the Main Thread",[27,55696,55697],{},"Want to see this in action? Try running this in your browser console:",[128,55699,55701],{"className":130,"code":55700,"language":132,"meta":133,"style":133},"console.time(\"localStorage writes\");\n\nfor (let i = 0; i \u003C 100000; i++) {\n    localStorage.setItem(`key-${i}`, `value-${i}`);\n}\n\nconsole.timeEnd(\"localStorage writes\");\nalert(\"Finished writing to localStorage!\");\n",[22,55702,55703,55717,55721,55751,55778,55782,55786,55799],{"__ignoreMap":133},[137,55704,55705,55707,55710,55712,55715],{"class":139,"line":140},[137,55706,1436],{"class":157},[137,55708,55709],{"class":147},"time",[137,55711,356],{"class":157},[137,55713,55714],{"class":284},"\"localStorage writes\"",[137,55716,1502],{"class":157},[137,55718,55719],{"class":139,"line":173},[137,55720,516],{"emptyLinePlaceholder":515},[137,55722,55723,55726,55728,55730,55733,55735,55737,55739,55741,55744,55747,55749],{"class":139,"line":188},[137,55724,55725],{"class":143},"for",[137,55727,158],{"class":157},[137,55729,11498],{"class":143},[137,55731,55732],{"class":157}," i ",[137,55734,253],{"class":143},[137,55736,7687],{"class":364},[137,55738,21483],{"class":157},[137,55740,4033],{"class":143},[137,55742,55743],{"class":364}," 100000",[137,55745,55746],{"class":157},"; i",[137,55748,12683],{"class":143},[137,55750,170],{"class":157},[137,55752,55753,55756,55758,55760,55763,55765,55767,55769,55772,55774,55776],{"class":139,"line":269},[137,55754,55755],{"class":157},"    localStorage.",[137,55757,55634],{"class":147},[137,55759,356],{"class":157},[137,55761,55762],{"class":284},"`key-${",[137,55764,53918],{"class":157},[137,55766,4706],{"class":284},[137,55768,164],{"class":157},[137,55770,55771],{"class":284},"`value-${",[137,55773,53918],{"class":157},[137,55775,4706],{"class":284},[137,55777,1502],{"class":157},[137,55779,55780],{"class":139,"line":278},[137,55781,510],{"class":157},[137,55783,55784],{"class":139,"line":291},[137,55785,516],{"emptyLinePlaceholder":515},[137,55787,55788,55790,55793,55795,55797],{"class":139,"line":297},[137,55789,1436],{"class":157},[137,55791,55792],{"class":147},"timeEnd",[137,55794,356],{"class":157},[137,55796,55714],{"class":284},[137,55798,1502],{"class":157},[137,55800,55801,55804,55806,55809],{"class":139,"line":302},[137,55802,55803],{"class":147},"alert",[137,55805,356],{"class":157},[137,55807,55808],{"class":284},"\"Finished writing to localStorage!\"",[137,55810,1502],{"class":157},[27,55812,55813,55814,55817,55818,55820],{},"You'll notice your browser ",[42,55815,55816],{},"freezes"," for a few seconds before the alert pops up. That's because every ",[22,55819,55634],{}," call blocks everything else - nothing can run until all those writes finish. Not a great user experience, right? 🙂",[104,55822,55824,55825],{"id":55823},"a-better-way-indexeddb","A Better Way: ",[22,55826,55827],{},"IndexedDB",[27,55829,55830,55832,55833,164,55836,14528,55839,55842],{},[22,55831,55827],{}," is a built-in browser database that's designed for ",[42,55834,55835],{},"asynchronous",[42,55837,55838],{},"transactional",[42,55840,55841],{},"large-scale"," data storage. It might sound intimidating at first, but it's easier than you think!",[27,55844,55845],{},"Here's how you can do the same thing we just tried, but without freesing the UI:",[128,55847,55849],{"className":130,"code":55848,"language":132,"meta":133,"style":133},"function openDB() {\n    return new Promise((resolve, reject) => {\n        const request = indexedDB.open(\"TestDB\", 1);\n        request.onupgradeneeded = () => {\n            request.result.createObjectStore(\"store\");\n        };\n        request.onsuccess = () => resolve(request.result);\n        request.onerror = () => reject(request.error);\n    });\n}\n\nasync function saveToIndexedDB() {\n    console.time(\"IndexedDB writes\");\n    const db = await openDB();\n\n    const tx = db.transaction(\"store\", \"readwrite\");\n    const store = tx.objectStore(\"store\");\n\n    for (let i = 0; i \u003C 10000; i++) {\n        store.put(`value-${i}`, `key-${i}`);\n    }\n\n    tx.oncomplete = () => {\n        console.timeEnd(\"IndexedDB writes\");\n        alert(\"Finished writing to IndexedDB!\");\n    };\n}\n\nsaveToIndexedDB();\n",[22,55850,55851,55860,55883,55907,55923,55938,55942,55961,55979,55983,55987,55991,56002,56015,56029,56033,56059,56080,56084,56111,56137,56141,56145,56161,56173,56185,56189,56193,56197],{"__ignoreMap":133},[137,55852,55853,55855,55858],{"class":139,"line":140},[137,55854,483],{"class":143},[137,55856,55857],{"class":147}," openDB",[137,55859,275],{"class":157},[137,55861,55862,55864,55866,55868,55870,55872,55874,55877,55879,55881],{"class":139,"line":173},[137,55863,176],{"class":143},[137,55865,1426],{"class":143},[137,55867,14116],{"class":364},[137,55869,2774],{"class":157},[137,55871,48591],{"class":161},[137,55873,164],{"class":157},[137,55875,55876],{"class":161},"reject",[137,55878,219],{"class":157},[137,55880,222],{"class":143},[137,55882,256],{"class":157},[137,55884,55885,55887,55889,55891,55894,55896,55898,55901,55903,55905],{"class":139,"line":188},[137,55886,3008],{"class":143},[137,55888,19801],{"class":364},[137,55890,151],{"class":143},[137,55892,55893],{"class":157}," indexedDB.",[137,55895,3954],{"class":147},[137,55897,356],{"class":157},[137,55899,55900],{"class":284},"\"TestDB\"",[137,55902,164],{"class":157},[137,55904,6065],{"class":364},[137,55906,1502],{"class":157},[137,55908,55909,55912,55915,55917,55919,55921],{"class":139,"line":269},[137,55910,55911],{"class":157},"        request.",[137,55913,55914],{"class":147},"onupgradeneeded",[137,55916,151],{"class":143},[137,55918,1484],{"class":157},[137,55920,222],{"class":143},[137,55922,256],{"class":157},[137,55924,55925,55928,55931,55933,55936],{"class":139,"line":278},[137,55926,55927],{"class":157},"            request.result.",[137,55929,55930],{"class":147},"createObjectStore",[137,55932,356],{"class":157},[137,55934,55935],{"class":284},"\"store\"",[137,55937,1502],{"class":157},[137,55939,55940],{"class":139,"line":291},[137,55941,1507],{"class":157},[137,55943,55944,55946,55949,55951,55953,55955,55958],{"class":139,"line":297},[137,55945,55911],{"class":157},[137,55947,55948],{"class":147},"onsuccess",[137,55950,151],{"class":143},[137,55952,1484],{"class":157},[137,55954,222],{"class":143},[137,55956,55957],{"class":147}," resolve",[137,55959,55960],{"class":157},"(request.result);\n",[137,55962,55963,55965,55967,55969,55971,55973,55976],{"class":139,"line":302},[137,55964,55911],{"class":157},[137,55966,50752],{"class":147},[137,55968,151],{"class":143},[137,55970,1484],{"class":157},[137,55972,222],{"class":143},[137,55974,55975],{"class":147}," reject",[137,55977,55978],{"class":157},"(request.error);\n",[137,55980,55981],{"class":139,"line":662},[137,55982,2832],{"class":157},[137,55984,55985],{"class":139,"line":667},[137,55986,510],{"class":157},[137,55988,55989],{"class":139,"line":786},[137,55990,516],{"emptyLinePlaceholder":515},[137,55992,55993,55995,55997,56000],{"class":139,"line":798},[137,55994,15050],{"class":143},[137,55996,154],{"class":143},[137,55998,55999],{"class":147}," saveToIndexedDB",[137,56001,275],{"class":157},[137,56003,56004,56006,56008,56010,56013],{"class":139,"line":803},[137,56005,493],{"class":157},[137,56007,55709],{"class":147},[137,56009,356],{"class":157},[137,56011,56012],{"class":284},"\"IndexedDB writes\"",[137,56014,1502],{"class":157},[137,56016,56017,56019,56021,56023,56025,56027],{"class":139,"line":931},[137,56018,4177],{"class":143},[137,56020,48654],{"class":364},[137,56022,151],{"class":143},[137,56024,15069],{"class":143},[137,56026,55857],{"class":147},[137,56028,924],{"class":157},[137,56030,56031],{"class":139,"line":1568},[137,56032,516],{"emptyLinePlaceholder":515},[137,56034,56035,56037,56040,56042,56045,56048,56050,56052,56054,56057],{"class":139,"line":1573},[137,56036,4177],{"class":143},[137,56038,56039],{"class":364}," tx",[137,56041,151],{"class":143},[137,56043,56044],{"class":157}," db.",[137,56046,56047],{"class":147},"transaction",[137,56049,356],{"class":157},[137,56051,55935],{"class":284},[137,56053,164],{"class":157},[137,56055,56056],{"class":284},"\"readwrite\"",[137,56058,1502],{"class":157},[137,56060,56061,56063,56066,56068,56071,56074,56076,56078],{"class":139,"line":1578},[137,56062,4177],{"class":143},[137,56064,56065],{"class":364}," store",[137,56067,151],{"class":143},[137,56069,56070],{"class":157}," tx.",[137,56072,56073],{"class":147},"objectStore",[137,56075,356],{"class":157},[137,56077,55935],{"class":284},[137,56079,1502],{"class":157},[137,56081,56082],{"class":139,"line":1588},[137,56083,516],{"emptyLinePlaceholder":515},[137,56085,56086,56088,56090,56092,56094,56096,56098,56100,56102,56105,56107,56109],{"class":139,"line":1601},[137,56087,21473],{"class":143},[137,56089,158],{"class":157},[137,56091,11498],{"class":143},[137,56093,55732],{"class":157},[137,56095,253],{"class":143},[137,56097,7687],{"class":364},[137,56099,21483],{"class":157},[137,56101,4033],{"class":143},[137,56103,56104],{"class":364}," 10000",[137,56106,55746],{"class":157},[137,56108,12683],{"class":143},[137,56110,170],{"class":157},[137,56112,56113,56116,56119,56121,56123,56125,56127,56129,56131,56133,56135],{"class":139,"line":3802},[137,56114,56115],{"class":157},"        store.",[137,56117,56118],{"class":147},"put",[137,56120,356],{"class":157},[137,56122,55771],{"class":284},[137,56124,53918],{"class":157},[137,56126,4706],{"class":284},[137,56128,164],{"class":157},[137,56130,55762],{"class":284},[137,56132,53918],{"class":157},[137,56134,4706],{"class":284},[137,56136,1502],{"class":157},[137,56138,56139],{"class":139,"line":3808},[137,56140,294],{"class":157},[137,56142,56143],{"class":139,"line":3822},[137,56144,516],{"emptyLinePlaceholder":515},[137,56146,56147,56150,56153,56155,56157,56159],{"class":139,"line":3827},[137,56148,56149],{"class":157},"    tx.",[137,56151,56152],{"class":147},"oncomplete",[137,56154,151],{"class":143},[137,56156,1484],{"class":157},[137,56158,222],{"class":143},[137,56160,256],{"class":157},[137,56162,56163,56165,56167,56169,56171],{"class":139,"line":3832},[137,56164,350],{"class":157},[137,56166,55792],{"class":147},[137,56168,356],{"class":157},[137,56170,56012],{"class":284},[137,56172,1502],{"class":157},[137,56174,56175,56178,56180,56183],{"class":139,"line":3840},[137,56176,56177],{"class":147},"        alert",[137,56179,356],{"class":157},[137,56181,56182],{"class":284},"\"Finished writing to IndexedDB!\"",[137,56184,1502],{"class":157},[137,56186,56187],{"class":139,"line":3846},[137,56188,1892],{"class":157},[137,56190,56191],{"class":139,"line":3861},[137,56192,510],{"class":157},[137,56194,56195],{"class":139,"line":3883},[137,56196,516],{"emptyLinePlaceholder":515},[137,56198,56199,56202],{"class":139,"line":3896},[137,56200,56201],{"class":147},"saveToIndexedDB",[137,56203,924],{"class":157},[27,56205,56206,56207,56210],{},"This version uses ",[42,56208,56209],{},"asynchronous transactions",", which means the browser stays responsive while your data is being written.",[104,56212,56214,56215],{"id":56213},"even-easier-localforage","Even Easier: ",[22,56216,56217],{},"localForage",[27,56219,56220,56221,3596,56224,56228,56229,56231,56232,894],{},"Now, if you're like me and love simplicity, you're going to ",[30,56222,56223],{},"love",[45,56225,56217],{"href":56226,"target":2716,"rel":56227},"https:\u002F\u002Fgithub.com\u002FlocalForage\u002FlocalForage",[2718,2719],". It's a lightweight wrapper around ",[22,56230,55827],{}," (with fallbacks to WebSQL or localStorage) that gives you a clean, ",[42,56233,56234],{},"Promise-based API",[128,56236,56238],{"className":8665,"code":56237,"language":8667,"meta":133,"style":133},"npm install localforage\n",[22,56239,56240],{"__ignoreMap":133},[137,56241,56242,56244,56246],{"class":139,"line":140},[137,56243,9536],{"class":147},[137,56245,10268],{"class":284},[137,56247,56248],{"class":284}," localforage\n",[128,56250,56252],{"className":130,"code":56251,"language":132,"meta":133,"style":133},"import localforage from \"localforage\";\n\nasync function saveToLocalForage() {\n    console.time(\"localForage writes\");\n\n    const promises = [];\n    for (let i = 0; i \u003C 10000; i++) {\n        promises.push(localforage.setItem(`key-${i}`, `value-${i}`));\n    }\n\n    await Promise.all(promises);\n    console.timeEnd(\"localForage writes\");\n    alert(\"Finished writing to localForage!\");\n}\n\nsaveToLocalForage();\n",[22,56253,56254,56268,56272,56283,56296,56300,56311,56337,56367,56371,56375,56388,56400,56412,56416,56420],{"__ignoreMap":133},[137,56255,56256,56258,56261,56263,56266],{"class":139,"line":140},[137,56257,10287],{"class":143},[137,56259,56260],{"class":157}," localforage ",[137,56262,10954],{"class":143},[137,56264,56265],{"class":284}," \"localforage\"",[137,56267,3276],{"class":157},[137,56269,56270],{"class":139,"line":173},[137,56271,516],{"emptyLinePlaceholder":515},[137,56273,56274,56276,56278,56281],{"class":139,"line":188},[137,56275,15050],{"class":143},[137,56277,154],{"class":143},[137,56279,56280],{"class":147}," saveToLocalForage",[137,56282,275],{"class":157},[137,56284,56285,56287,56289,56291,56294],{"class":139,"line":269},[137,56286,493],{"class":157},[137,56288,55709],{"class":147},[137,56290,356],{"class":157},[137,56292,56293],{"class":284},"\"localForage writes\"",[137,56295,1502],{"class":157},[137,56297,56298],{"class":139,"line":278},[137,56299,516],{"emptyLinePlaceholder":515},[137,56301,56302,56304,56307,56309],{"class":139,"line":291},[137,56303,4177],{"class":143},[137,56305,56306],{"class":364}," promises",[137,56308,151],{"class":143},[137,56310,8556],{"class":157},[137,56312,56313,56315,56317,56319,56321,56323,56325,56327,56329,56331,56333,56335],{"class":139,"line":297},[137,56314,21473],{"class":143},[137,56316,158],{"class":157},[137,56318,11498],{"class":143},[137,56320,55732],{"class":157},[137,56322,253],{"class":143},[137,56324,7687],{"class":364},[137,56326,21483],{"class":157},[137,56328,4033],{"class":143},[137,56330,56104],{"class":364},[137,56332,55746],{"class":157},[137,56334,12683],{"class":143},[137,56336,170],{"class":157},[137,56338,56339,56342,56344,56347,56349,56351,56353,56355,56357,56359,56361,56363,56365],{"class":139,"line":302},[137,56340,56341],{"class":157},"        promises.",[137,56343,8583],{"class":147},[137,56345,56346],{"class":157},"(localforage.",[137,56348,55634],{"class":147},[137,56350,356],{"class":157},[137,56352,55762],{"class":284},[137,56354,53918],{"class":157},[137,56356,4706],{"class":284},[137,56358,164],{"class":157},[137,56360,55771],{"class":284},[137,56362,53918],{"class":157},[137,56364,4706],{"class":284},[137,56366,8614],{"class":157},[137,56368,56369],{"class":139,"line":662},[137,56370,294],{"class":157},[137,56372,56373],{"class":139,"line":667},[137,56374,516],{"emptyLinePlaceholder":515},[137,56376,56377,56379,56381,56383,56385],{"class":139,"line":786},[137,56378,15100],{"class":143},[137,56380,14116],{"class":364},[137,56382,1017],{"class":157},[137,56384,49391],{"class":147},[137,56386,56387],{"class":157},"(promises);\n",[137,56389,56390,56392,56394,56396,56398],{"class":139,"line":798},[137,56391,493],{"class":157},[137,56393,55792],{"class":147},[137,56395,356],{"class":157},[137,56397,56293],{"class":284},[137,56399,1502],{"class":157},[137,56401,56402,56405,56407,56410],{"class":139,"line":803},[137,56403,56404],{"class":147},"    alert",[137,56406,356],{"class":157},[137,56408,56409],{"class":284},"\"Finished writing to localForage!\"",[137,56411,1502],{"class":157},[137,56413,56414],{"class":139,"line":931},[137,56415,510],{"class":157},[137,56417,56418],{"class":139,"line":1568},[137,56419,516],{"emptyLinePlaceholder":515},[137,56421,56422,56425],{"class":139,"line":1573},[137,56423,56424],{"class":147},"saveToLocalForage",[137,56426,924],{"class":157},[27,56428,56206,56429,56432,56433,56435],{},[42,56430,56431],{},"IndexedDB under the hood"," but gives you the same simple API style as ",[22,56434,55593],{}," - without any of the blocking or complexity. Best of both worlds!",[104,56437,56439,56440,56442],{"id":56438},"why-localstorage-fails-for-transactions","Why ",[22,56441,55593],{}," Fails for Transactions",[27,56444,56445,56446,56448,56449,56451],{},"When we talk about ",[42,56447,55600],{},", we usually mean multiple operations that need to succeed or fail together. Unfortunately, ",[22,56450,55593],{}," just isn't built for that. Let me show you why:",[45740,56453,56454,56464],{},[45743,56455,56456],{},[45746,56457,56458,56461],{},[45749,56459,56460],{},"Problem",[45749,56462,56463],{},"Description",[45762,56465,56466,56477,56488,56499,56510],{},[45746,56467,56468,56474],{},[45767,56469,56470,56471],{},"🧵",[42,56472,56473],{},"Synchronous & blocking",[45767,56475,56476],{},"Freezes the main thread during heavy writes.",[45746,56478,56479,56485],{},[45767,56480,56481,56482],{},"❌",[42,56483,56484],{},"No transaction support",[45767,56486,56487],{},"You can't rollback or commit multiple changes atomically.",[45746,56489,56490,56496],{},[45767,56491,56492,56493],{},"⚔️",[42,56494,56495],{},"No concurrency control",[45767,56497,56498],{},"Two tabs or scripts can easily overwrite each other's data.",[45746,56500,56501,56507],{},[45767,56502,56503,56504],{},"📦",[42,56505,56506],{},"Tiny storage limit",[45767,56508,56509],{},"Typically 5–10 MB total.",[45746,56511,56512,56518],{},[45767,56513,56514,56515],{},"🔓",[42,56516,56517],{},"No security",[45767,56519,56520],{},"Data is stored in plain text, accessible to any script on the domain.",[3244,56522,56523],{},[27,56524,56525,56526,56528,56529,3955,56532],{},"In short, ",[22,56527,55593],{}," is perfectly fine for ",[30,56530,56531],{},"simple preferences",[30,56533,56534],{},"light caching",[27,56536,56537,56538,56540,56541,56543,56544,56546],{},"Unlike ",[22,56539,55593],{},", which saves each item one by one, ",[22,56542,55827],{}," handles your data in a much safer way. It uses something called ",[30,56545,55600],{},", which simply means that all your data changes happen together as a single, reliable action.",[27,56548,56549,56550,56553],{},"If everything goes well, the changes are saved. But if something goes wrong halfway through - say, your browser crashes or one of the operations fails - ",[42,56551,56552],{},"nothing gets written",". Your data stays clean, consistent, and exactly how it was before.",[104,56555,56557],{"id":56556},"quick-summary","Quick Summary",[27,56559,56560],{},"Here's a user-friendly table I put together to give you a clean and easy way to compare the pros and cons of the three options we discussed above:",[45740,56562,56563,56582],{},[45743,56564,56565],{},[45746,56566,56567,56570,56574,56578],{},[45749,56568,56569],{},"Feature",[45749,56571,56572],{},[22,56573,55593],{},[45749,56575,56576],{},[22,56577,55827],{},[45749,56579,56580],{},[22,56581,56217],{},[45762,56583,56584,56597,56609,56622,56636],{},[45746,56585,56586,56589,56592,56595],{},[45767,56587,56588],{},"Blocking",[45767,56590,56591],{},"✅ Yes",[45767,56593,56594],{},"❌ No",[45767,56596,56594],{},[45746,56598,56599,56602,56604,56606],{},[45767,56600,56601],{},"Transactions",[45767,56603,56594],{},[45767,56605,56591],{},[45767,56607,56608],{},"✅ Yes (via IndexedDB)",[45746,56610,56611,56614,56617,56620],{},[45767,56612,56613],{},"Concurrency",[45767,56615,56616],{},"❌ Unsafe",[45767,56618,56619],{},"✅ Safe",[45767,56621,56619],{},[45746,56623,56624,56627,56630,56633],{},[45767,56625,56626],{},"Storage Limit",[45767,56628,56629],{},"~5–10 MB",[45767,56631,56632],{},"Hundreds of MBs+",[45767,56634,56635],{},"Same as IndexedDB",[45746,56637,56638,56640,56643,56646],{},[45767,56639,45751],{},[45767,56641,56642],{},"Small settings",[45767,56644,56645],{},"Complex, large data",[45767,56647,56648],{},"Simple async API",[27,56650,56651,56652],{},"You can find all the working code examples from this article in ",[45,56653,56656],{"href":56654,"target":2716,"rel":56655},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fwhy-you-shouldnt-use-localstorage-for-transactions-and-what-to-use-instead?tab=readme-ov-file",[2718,2719],"my GitHub account",[2617,56658,56659],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":133,"searchDepth":173,"depth":173,"links":56661},[56662,56664,56665,56667,56669,56671],{"id":55612,"depth":173,"text":56663},"How localStorage Works",{"id":55693,"depth":173,"text":55694},{"id":55823,"depth":173,"text":56666},"A Better Way: IndexedDB",{"id":56213,"depth":173,"text":56668},"Even Easier: localForage",{"id":56438,"depth":173,"text":56670},"Why localStorage Fails for Transactions",{"id":56556,"depth":173,"text":56557},"localStorage may be convenient, but it’s not built for handling complex operations, concurrent writes, or large volumes of data. What seems simple at first can quickly lead to frozen UIs and unreliable transactions. In this article, we dive into the hidden limitations of localStorage and explore how modern, asynchronous storage solutions like IndexedDB and localForage deliver the speed, safety, and scalability that today’s web apps need.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1762215611\u002Fblog\u002Fwhy-you-shouldnt-use-localstorage-for-transactions-and-what-to-use-instead\u002Fwhy-you-shouldnt-use-localstorage-for-transactions-and-what-to-use-instead_kqnbsb",[56675,56676,56677,56678,56679,56680,56681,56682,56683,56684,56685,56686,56687,56688,56689,56690,56691,56692],"localStorage transactions","localStorage vs IndexedDB","localStorage performance issues","IndexedDB tutorial","localForage example","browser storage alternatives","JavaScript data storage","asynchronous browser storage","why not use localStorage","IndexedDB for beginners","IndexedDB vs localStorage performance","web app data storage best practices","localStorage blocking main thread","improve browser performance","safe data storage in web apps","JavaScript async storage APIs","non-blocking browser storage","frontend performance optimization",{},"\u002F2025\u002F11\u002F04\u002Fwhy-you-shouldnt-use-localstorage-for-transactions-and-what-to-use-instead","4th November 2025",{"title":55556,"description":56672},"2025\u002F11\u002F04\u002Fwhy-you-shouldnt-use-localstorage-for-transactions-and-what-to-use-instead","8Boa-k181HVHKRvOXPj8drGCD9kyo_LR64hXm6RGYns",{"id":56700,"title":56701,"articleTags":56702,"author":11,"blog":12,"body":56703,"description":57018,"extension":2649,"image":57019,"keywords":57020,"meta":57035,"navigation":515,"path":57036,"published":57037,"readTime":140,"seo":57038,"stem":57039,"type":2662,"__hash__":57040},"content\u002F2025\u002F11\u002F08\u002Fmigrating-your-homebrew-setup-to-a-new-mac-with-one-command.md","Migrating Your Homebrew Setup to a New Mac with One Command",[52208,52207,22225],{"type":14,"value":56704,"toc":57009},[56705,56708,56722,56724,56728,56733,56739,56742,56745,56751,56758,56764,56767,56773,56778,56806,56813,56916,56922,56925,56948,56953,56956,56958,56964,56967,56970,57006],[17,56706,56701],{"id":56707},"migrating-your-homebrew-setup-to-a-new-mac-with-one-command",[27,56709,56710],{},[30,56711,56712,36,56714,40,56716],{},[33,56713],{"value":35},[33,56715],{"value":39},[42,56717,56718],{},[45,56719,56720],{"href":47},[33,56721],{"value":50},[52,56723],{":tags":54},[56,56725],{":audio-src":56726,":transcript-src":56727},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F11\u002F08\u002Fmigrating-your-homebrew-setup-to-a-new-mac-with-one-command\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F11\u002F08\u002Fmigrating-your-homebrew-setup-to-a-new-mac-with-one-command\u002Fsummary.json",[27,56729,56730],{},[63,56731],{"alt":12847,"src":56732},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1762580731\u002Fblog\u002Fmigrating-your-homebrew-setup-to-a-new-mac-with-one-command\u002Fmigrating-your-homebrew-setup-to-a-new-mac-with-one-command_krwbvw",[104,56734,56736],{"id":56735},"how-to-backup-and-restore-homebrew-packages-on-macos",[42,56737,56738],{},"How to Backup and Restore Homebrew Packages on macOS",[27,56740,56741],{},"Setting up a new Mac can be exciting - but reinstalling all your tools, libraries, and apps? Not so much. If you use Homebrew, there’s a super clean way to transfer everything from your old Mac to your new one using a single file.",[27,56743,56744],{},"In this guide, I’ll show you how to export everything installed via Homebrew, and then import it onto your new machine.",[104,56746,56748],{"id":56747},"what-is-homebrew",[42,56749,56750],{},"What Is Homebrew?",[27,56752,56753,56754,1017],{},"For the uninitiated, Homebrew is the go-to package manager for macOS. Developers use it to install and manage tools like Node.js, Python, Git, Docker, and even GUI apps like Google Chrome or VSCode. You can read more ",[45,56755,10647],{"href":56756,"target":2716,"rel":56757},"https:\u002F\u002Fbrew.sh",[2718,2719],[104,56759,56761],{"id":56760},"exporting-your-homebrew-setup-old-mac",[42,56762,56763],{},"📤 Exporting Your Homebrew Setup (Old Mac)",[27,56765,56766],{},"Homebrew has a built-in command called brew bundle. This lets you export your entire setup into a file called Brewfile.",[123,56768,56770],{"id":56769},"run-this-on-your-old-mac",[42,56771,56772],{},"Run this on your old Mac:",[27,56774,56775],{},[22,56776,56777],{},"brew bundle dump --file=~\u002FBrewfile --describe",[1003,56779,56780,56786,56792],{},[1006,56781,56782,56785],{},[22,56783,56784],{},"--file=~\u002FBrewfile"," saves the list to your home directory.",[1006,56787,56788,56791],{},[22,56789,56790],{},"—-describe"," adds comments to explain what each package does.",[1006,56793,56794,56795],{},"This includes:\n",[1003,56796,56797,56800,56803],{},[1006,56798,56799],{},"brew packages (CLI tools)",[1006,56801,56802],{},"cask apps (GUI apps like Chrome)",[1006,56804,56805],{},"tap sources",[27,56807,56808,56809,56812],{},"The output of the ",[22,56810,56811],{},"Brewfile"," looks something like this:",[128,56814,56816],{"className":8665,"code":56815,"language":8667,"meta":133,"style":133},"# Fast, disk space efficient package manager\nbrew \"pnpm\"\n# Python package management tool\nbrew \"poetry\"\n# Object-relational database system\nbrew \"postgresql@14\"\n# Python version management\nbrew \"pyenv\"\n# Password manager that keeps all passwords secure behind one password\ncask \"1password\"\n# Universal database tool and SQL client\ncask \"dbeaver-community\"\n# Voice and text chat software\ncask \"discord\"\n# App to build and share containerised applications and microservices\ncask \"docker-desktop\"\n",[22,56817,56818,56823,56831,56836,56843,56848,56855,56860,56867,56872,56880,56885,56892,56897,56904,56909],{"__ignoreMap":133},[137,56819,56820],{"class":139,"line":140},[137,56821,56822],{"class":308},"# Fast, disk space efficient package manager\n",[137,56824,56825,56828],{"class":139,"line":173},[137,56826,56827],{"class":147},"brew",[137,56829,56830],{"class":284}," \"pnpm\"\n",[137,56832,56833],{"class":139,"line":188},[137,56834,56835],{"class":308},"# Python package management tool\n",[137,56837,56838,56840],{"class":139,"line":269},[137,56839,56827],{"class":147},[137,56841,56842],{"class":284}," \"poetry\"\n",[137,56844,56845],{"class":139,"line":278},[137,56846,56847],{"class":308},"# Object-relational database system\n",[137,56849,56850,56852],{"class":139,"line":291},[137,56851,56827],{"class":147},[137,56853,56854],{"class":284}," \"postgresql@14\"\n",[137,56856,56857],{"class":139,"line":297},[137,56858,56859],{"class":308},"# Python version management\n",[137,56861,56862,56864],{"class":139,"line":302},[137,56863,56827],{"class":147},[137,56865,56866],{"class":284}," \"pyenv\"\n",[137,56868,56869],{"class":139,"line":662},[137,56870,56871],{"class":308},"# Password manager that keeps all passwords secure behind one password\n",[137,56873,56874,56877],{"class":139,"line":667},[137,56875,56876],{"class":147},"cask",[137,56878,56879],{"class":284}," \"1password\"\n",[137,56881,56882],{"class":139,"line":786},[137,56883,56884],{"class":308},"# Universal database tool and SQL client\n",[137,56886,56887,56889],{"class":139,"line":798},[137,56888,56876],{"class":147},[137,56890,56891],{"class":284}," \"dbeaver-community\"\n",[137,56893,56894],{"class":139,"line":803},[137,56895,56896],{"class":308},"# Voice and text chat software\n",[137,56898,56899,56901],{"class":139,"line":931},[137,56900,56876],{"class":147},[137,56902,56903],{"class":284}," \"discord\"\n",[137,56905,56906],{"class":139,"line":1568},[137,56907,56908],{"class":308},"# App to build and share containerised applications and microservices\n",[137,56910,56911,56913],{"class":139,"line":1573},[137,56912,56876],{"class":147},[137,56914,56915],{"class":284}," \"docker-desktop\"\n",[104,56917,56919],{"id":56918},"importing-on-your-new-mac",[42,56920,56921],{},"📥 Importing on Your New Mac",[27,56923,56924],{},"After setting up your new Mac:",[2569,56926,56927,56936,56945],{},[1006,56928,56929,56930,56933],{},"Install Homebrew first - ",[45,56931],{"href":56756,"target":2716,"rel":56932},[2718,2719],[45,56934,56756],{"href":56756,"rel":56935},[10924],[1006,56937,56938,56939,56941,56942,56944],{},"Transfer your ",[22,56940,56811],{},"\nCopy your ",[22,56943,56811],{}," to your new Mac",[1006,56946,56947],{},"Install everything from the Brewfile:",[27,56949,56950],{},[22,56951,56952],{},"brew bundle --file=~\u002FBrewfile",[27,56954,56955],{},"Homebrew will read the file and install everything it finds - bringing your dev environment back to life in minutes.",[104,56957,2567],{"id":2566},[27,56959,56960,56961,56963],{},"Reinstalling every package and app manually can take hours. But with the magic of brew bundle, your transition to a new machine is painless. Whether you’re upgrading your MacBook or setting up a secondary work machine, having a ",[22,56962,56811],{}," in your backup is a huge time-saver.",[27,56965,56966],{},"So next time you run brew install, think of it as an investment in your future self.",[27,56968,56969],{},"I've put together a table with the two commands you'll need for this entire migration process.",[45740,56971,56972,56986],{},[45743,56973,56974],{},[45746,56975,56976,56981],{},[45749,56977,56978],{},[42,56979,56980],{},"Action",[45749,56982,56983],{},[42,56984,56985],{},"Command",[45762,56987,56988,56997],{},[45746,56989,56990,56993],{},[45767,56991,56992],{},"Export",[45767,56994,56995],{},[22,56996,56777],{},[45746,56998,56999,57002],{},[45767,57000,57001],{},"Import",[45767,57003,57004],{},[22,57005,56952],{},[2617,57007,57008],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":133,"searchDepth":173,"depth":173,"links":57010},[57011,57012,57013,57016,57017],{"id":56735,"depth":173,"text":56738},{"id":56747,"depth":173,"text":56750},{"id":56760,"depth":173,"text":56763,"children":57014},[57015],{"id":56769,"depth":188,"text":56772},{"id":56918,"depth":173,"text":56921},{"id":2566,"depth":173,"text":2567},"Easily migrate your entire Homebrew setup to a new Mac with just one simple command. This step-by-step guide shows you how to back up and restore all your Homebrew packages, apps, and developer tools using a Brewfile - saving you hours of manual reinstalls and setup time when moving to a new machine.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1762580731\u002Fblog\u002Fmigrating-your-homebrew-setup-to-a-new-mac-with-one-command\u002Fmigrating-your-homebrew-setup-to-a-new-mac-with-one-command_krwbvw",[56675,57021,57022,56811,57023,57024,57025,57026,57027,57028,57029,57030,57031,57032,57033,57034],"Homebrew","macOS","migrate Homebrew","backup Homebrew","restore Homebrew","brew bundle","transfer Homebrew packages","Mac setup automation","macOS developer tools","Homebrew export","Homebrew import","brewfile tutorial","brew bundle dump","brew bundle install",{},"\u002F2025\u002F11\u002F08\u002Fmigrating-your-homebrew-setup-to-a-new-mac-with-one-command","8th November 2025",{"title":56701,"description":57018},"2025\u002F11\u002F08\u002Fmigrating-your-homebrew-setup-to-a-new-mac-with-one-command","-pl0EAkbDCtFnW-60SswYgh4MKHDkVibi8-VOP9aIBw",{"id":57042,"title":57043,"articleTags":57044,"author":11,"blog":12,"body":57045,"description":59114,"extension":2649,"image":59115,"keywords":59116,"meta":59129,"navigation":515,"path":59130,"published":59131,"readTime":291,"seo":59132,"stem":59133,"type":2662,"__hash__":59134},"content\u002F2025\u002F11\u002F13\u002Fturning-markdown-into-podcasts-with-openai-agents.md","Turning Markdown into Podcasts with OpenAI Agents",[12817,27886,2669],{"type":14,"value":57046,"toc":59100},[57047,57050,57064,57066,57070,57075,57078,57087,57091,57094,57107,57157,57166,57170,57173,57205,57209,57212,57242,57248,57255,57259,57262,57307,57312,57318,57326,57371,57375,57378,57382,57385,57399,57402,57501,57507,57590,57594,57601,57674,57678,57681,57812,57819,57821,57824,57847,57850,58447,58450,58973,58977,58980,59016,59019,59052,59072,59076,59079,59082,59089,59097],[17,57048,57043],{"id":57049},"turning-markdown-into-podcasts-with-openai-agents",[27,57051,57052],{},[30,57053,57054,36,57056,40,57058],{},[33,57055],{"value":35},[33,57057],{"value":39},[42,57059,57060],{},[45,57061,57062],{"href":47},[33,57063],{"value":50},[52,57065],{":tags":54},[56,57067],{":audio-src":57068,":transcript-src":57069},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F11\u002F13\u002Fturning-markdown-into-podcasts-with-openai-agents\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F11\u002F13\u002Fturning-markdown-into-podcasts-with-openai-agents\u002Fsummary.json",[27,57071,57072],{},[63,57073],{"alt":12847,"src":57074},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1762858276\u002Fblog\u002Fturning-markdown-into-podcasts-with-openai-agents\u002Fturning-markdown-into-podcasts-with-openai-agents_aav3tt",[27,57076,57077],{},"I've been writing my blog entirely in Markdown for years. It's perfect for me as a developer - it's something we're familiar with, and it works great with static site generators. But turning those posts into podcasts style audio always felt like starting a whole second project. That was true five years ago. Now, with the new wave of LLMs and generative AI and the rise of agentic frameworks - that manual work has become much easier. You can easily build your own little team of AI helpers, ones that handle scriptwriting, editing, and even voiceover work. The result? A system that transforms my Markdown articles into polished MP3 podcasts in just a few minutes.",[27,57079,57080,57081,57086],{},"There are a lot of agentic frameworks out there, but in this guide, I'll walk you through exactly how I built this pipeline using OpenAI's ",[45,57082,57085],{"href":57083,"target":2716,"rel":57084},"https:\u002F\u002Fopenai.github.io\u002Fopenai-agents-js\u002F",[2718,2719],"Agents SDK"," - from Markdown to script to finished audio. You'll see how to set it up yourself, tweak it to match your style, and finally give your blog its own voice.",[104,57088,57090],{"id":57089},"how-the-idea-came-about","How the Idea Came About",[27,57092,57093],{},"I write a lot of technical articles packed with code examples. They're helpful for developers who want to dig into the details, but I started noticing not everyone has time to sit down and read through long posts. Some might prefer to listen on the go. Others just want a quick, conversational summary - and if they're more interested, they can proceed to the full read.",[27,57095,57096,57097,57100,57101,114,57104,57106],{},"So I built this AI-powered workflow proof of concept using a lot of vibe coding 🤓. You drop in a Markdown article, and out comes a ",[42,57098,57099],{},"podcast-ready MP3"," - complete with a friendly intro, natural pacing, and your choice of either a single host or a conversation between two hosts (I named them ",[42,57102,57103],{},"Aleks",[42,57105,900],{},") after me and my wife 🙂. And the whole process takes only a couple of minutes. Here's what these AI agents help us accomplish:",[1003,57108,57109,57122,57125],{},[1006,57110,57111,57112,164,57115,57118,57119,1017],{},"They keep the tone ",[42,57113,57114],{},"friendly",[42,57116,57117],{},"educational",", and genuinely ",[42,57120,57121],{},"entertaining",[1006,57123,57124],{},"They never robotically read code blocks - instead, they explain what the code actually does in plain English.",[1006,57126,57127,57128,164,57131,29088,57134,57137,57138,164,57141,164,57144,164,57147,164,57150,164,57153,57156],{},"They add natural touches like ",[42,57129,57130],{},"laughs",[42,57132,57133],{},"pauses",[42,57135,57136],{},"excitement"," to make it feel real. These vocal direction tags - ",[22,57139,57140],{},"[laughs]",[22,57142,57143],{},"[pause]",[22,57145,57146],{},"[whispers]",[22,57148,57149],{},"[excited]",[22,57151,57152],{},"[angrily]",[22,57154,57155],{},"[softly]"," - help the models add tonal expressions to the voice reading the script.",[3244,57158,57159],{},[27,57160,57161,57162,57165],{},"For reference, running the dual-speaker flow costs approximately ",[42,57163,57164],{},"$0.06 USD"," per article.",[104,57167,57169],{"id":57168},"what-youll-need","What You'll Need",[27,57171,57172],{},"Before we dive into the code, here's what you'll need to get started:",[1003,57174,57175,57184,57196,57202],{},[1006,57176,57177,57180,57181,57183],{},[42,57178,57179],{},"Node.js 22+"," - The ",[22,57182,29200],{}," runner needs modern ESM support.",[1006,57185,57186,57189,57190,114,57193,1017],{},[42,57187,57188],{},"An OpenAI API key"," - with access to ",[22,57191,57192],{},"gpt-4o",[22,57194,57195],{},"tts-1-hd",[1006,57197,57198,57201],{},[42,57199,57200],{},"A Markdown file"," - any blog post or article you want to convert.",[1006,57203,57204],{},"No prior experience with AI agents required - I'll walk you through everything.",[104,57206,57208],{"id":57207},"the-architecture-how-it-works","The Architecture: How It Works",[27,57210,57211],{},"Let me break down how this system works under the hood:",[2569,57213,57214,57220,57226,57232],{},[1006,57215,57216,57219],{},[42,57217,57218],{},"Scriptwriter Agent"," → Takes your Markdown and converts it into a natural-sounding podcast script.",[1006,57221,57222,57225],{},[42,57223,57224],{},"Editor Agent"," → Polishes the script to improve tone, pacing, and conversational flow.",[1006,57227,57228,57231],{},[42,57229,57230],{},"Text-to-Speech Engine"," → Transforms the polished script into actual audio.",[1006,57233,57234,57237,57238,57241],{},[42,57235,57236],{},"Output"," → You get a shiny new ",[22,57239,57240],{},"podcast_episode.mp3"," ready to share with the world.",[27,57243,57244],{},[63,57245],{"alt":57246,"src":57247},"Architecture","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1762858306\u002Fblog\u002Fturning-markdown-into-podcasts-with-openai-agents\u002FArchitecture_obtmz8",[27,57249,57250,57251,57254],{},"Think of it as your personal ",[42,57252,57253],{},"AI production team"," - where GPT handles the creative writing and storytelling, while the TTS model brings it to life with voice.",[104,57256,57258],{"id":57257},"setting-up-the-project","Setting Up the Project",[27,57260,57261],{},"Let's get your environment ready. First, create a new project:",[128,57263,57265],{"className":8665,"code":57264,"language":8667,"meta":133,"style":133},"mkdir blog-article-to-podcast && cd $_\nnpm init -y\nnpm install openai @openai\u002Fagents tsx\n",[22,57266,57267,57283,57292],{"__ignoreMap":133},[137,57268,57269,57272,57275,57278,57280],{"class":139,"line":140},[137,57270,57271],{"class":147},"mkdir",[137,57273,57274],{"class":284}," blog-article-to-podcast",[137,57276,57277],{"class":157}," && ",[137,57279,9558],{"class":364},[137,57281,57282],{"class":364}," $_\n",[137,57284,57285,57287,57289],{"class":139,"line":173},[137,57286,9536],{"class":147},[137,57288,9539],{"class":284},[137,57290,57291],{"class":364}," -y\n",[137,57293,57294,57296,57298,57301,57304],{"class":139,"line":188},[137,57295,9536],{"class":147},[137,57297,10268],{"class":284},[137,57299,57300],{"class":284}," openai",[137,57302,57303],{"class":284}," @openai\u002Fagents",[137,57305,57306],{"class":284}," tsx\n",[27,57308,23334,57309,57311],{},[22,57310,13489],{}," file and add your OpenAI key:",[128,57313,57316],{"className":57314,"code":57315,"language":5189},[5187],"OPENAI_API_KEY=sk-...\n",[22,57317,57315],{"__ignoreMap":133},[27,57319,57320,57321,57323,57324,9772],{},"Then update your ",[22,57322,5140],{}," to use ES modules and automatically load the ",[22,57325,13489],{},[128,57327,57329],{"className":5155,"code":57328,"language":5157,"meta":133,"style":133},"{\n    \"type\": \"module\",\n    \"scripts\": {\n        \"start\": \"tsx --env-file=.env .\u002Findex.ts\"\n    }\n}\n",[22,57330,57331,57335,57346,57353,57363,57367],{"__ignoreMap":133},[137,57332,57333],{"class":139,"line":140},[137,57334,15971],{"class":157},[137,57336,57337,57340,57342,57344],{"class":139,"line":173},[137,57338,57339],{"class":364},"    \"type\"",[137,57341,726],{"class":157},[137,57343,25777],{"class":284},[137,57345,1961],{"class":157},[137,57347,57348,57351],{"class":139,"line":188},[137,57349,57350],{"class":364},"    \"scripts\"",[137,57352,1819],{"class":157},[137,57354,57355,57358,57360],{"class":139,"line":269},[137,57356,57357],{"class":364},"        \"start\"",[137,57359,726],{"class":157},[137,57361,57362],{"class":284},"\"tsx --env-file=.env .\u002Findex.ts\"\n",[137,57364,57365],{"class":139,"line":278},[137,57366,294],{"class":157},[137,57368,57369],{"class":139,"line":291},[137,57370,510],{"class":157},[104,57372,57374],{"id":57373},"building-the-agents","Building the Agents",[27,57376,57377],{},"Now for my favourite part - giving each AI agent its specific role and personality.",[123,57379,57381],{"id":57380},"the-scriptwriters","The Scriptwriters",[27,57383,57384],{},"I created two versions to give you flexibility:",[1003,57386,57387,57393],{},[1006,57388,57389,57392],{},[42,57390,57391],{},"Single-host mode (Aleks only)"," - for a straightforward, solo narration",[1006,57394,57395,57398],{},[42,57396,57397],{},"Dual-host mode (Aleks & Nicole)"," - for a more dynamic, conversational feel",[27,57400,57401],{},"Both convert your Markdown into podcast-style narration, but the dual-host version adds a natural back-and-forth dialogue that makes technical content more engaging.",[128,57403,57405],{"className":13299,"code":57404,"language":13301,"meta":133,"style":133},"const scriptwriterSingle = new Agent({\n    name: \"ScriptwriterSingle\",\n    instructions: `\n    You are a professional tech podcast writer.\n    Convert a markdown blog article into an engaging single-speaker podcast script.\n    The speaker is Aleks, hosting a solo tech podcast.\n    Keep it friendly, educational, and conversational.\n    DO NOT read code out loud — explain what it does.\n    Include an intro and outro.\n    Use markers like [pause], [excited], or [laughs] where natural.\n    Format as JSON: { \"intro\": \"...\", \"content\": \"...\", \"outro\": \"...\" }\n  `,\n    model: \"gpt-4o\",\n});\n",[22,57406,57407,57423,57433,57440,57445,57450,57455,57460,57465,57470,57475,57480,57487,57497],{"__ignoreMap":133},[137,57408,57409,57411,57414,57416,57418,57421],{"class":139,"line":140},[137,57410,3077],{"class":143},[137,57412,57413],{"class":364}," scriptwriterSingle",[137,57415,151],{"class":143},[137,57417,1426],{"class":143},[137,57419,57420],{"class":147}," Agent",[137,57422,3175],{"class":157},[137,57424,57425,57428,57431],{"class":139,"line":173},[137,57426,57427],{"class":157},"    name: ",[137,57429,57430],{"class":284},"\"ScriptwriterSingle\"",[137,57432,1961],{"class":157},[137,57434,57435,57438],{"class":139,"line":188},[137,57436,57437],{"class":157},"    instructions: ",[137,57439,22062],{"class":284},[137,57441,57442],{"class":139,"line":269},[137,57443,57444],{"class":284},"    You are a professional tech podcast writer.\n",[137,57446,57447],{"class":139,"line":278},[137,57448,57449],{"class":284},"    Convert a markdown blog article into an engaging single-speaker podcast script.\n",[137,57451,57452],{"class":139,"line":291},[137,57453,57454],{"class":284},"    The speaker is Aleks, hosting a solo tech podcast.\n",[137,57456,57457],{"class":139,"line":297},[137,57458,57459],{"class":284},"    Keep it friendly, educational, and conversational.\n",[137,57461,57462],{"class":139,"line":302},[137,57463,57464],{"class":284},"    DO NOT read code out loud — explain what it does.\n",[137,57466,57467],{"class":139,"line":662},[137,57468,57469],{"class":284},"    Include an intro and outro.\n",[137,57471,57472],{"class":139,"line":667},[137,57473,57474],{"class":284},"    Use markers like [pause], [excited], or [laughs] where natural.\n",[137,57476,57477],{"class":139,"line":786},[137,57478,57479],{"class":284},"    Format as JSON: { \"intro\": \"...\", \"content\": \"...\", \"outro\": \"...\" }\n",[137,57481,57482,57485],{"class":139,"line":798},[137,57483,57484],{"class":284},"  `",[137,57486,1961],{"class":157},[137,57488,57489,57492,57495],{"class":139,"line":803},[137,57490,57491],{"class":157},"    model: ",[137,57493,57494],{"class":284},"\"gpt-4o\"",[137,57496,1961],{"class":157},[137,57498,57499],{"class":139,"line":931},[137,57500,5422],{"class":157},[27,57502,4737,57503,57506],{},[42,57504,57505],{},"dual-host version"," creates a conversation instead of narration:",[128,57508,57510],{"className":13299,"code":57509,"language":13301,"meta":133,"style":133},"const scriptwriterDual = new Agent({\n    name: \"ScriptwriterDual\",\n    instructions: `\n    You are a witty tech podcast writer.\n    Convert markdown into a conversation between Aleks and Nicole.\n    Keep it educational, funny, and natural.\n    Never read code verbatim — explain it.\n    Include intro\u002Foutro and use emotional markers sparingly.\n    Return JSON with intro, dialogue[], and outro.\n  `,\n    model: \"gpt-4o\",\n});\n",[22,57511,57512,57527,57536,57542,57547,57552,57557,57562,57567,57572,57578,57586],{"__ignoreMap":133},[137,57513,57514,57516,57519,57521,57523,57525],{"class":139,"line":140},[137,57515,3077],{"class":143},[137,57517,57518],{"class":364}," scriptwriterDual",[137,57520,151],{"class":143},[137,57522,1426],{"class":143},[137,57524,57420],{"class":147},[137,57526,3175],{"class":157},[137,57528,57529,57531,57534],{"class":139,"line":173},[137,57530,57427],{"class":157},[137,57532,57533],{"class":284},"\"ScriptwriterDual\"",[137,57535,1961],{"class":157},[137,57537,57538,57540],{"class":139,"line":188},[137,57539,57437],{"class":157},[137,57541,22062],{"class":284},[137,57543,57544],{"class":139,"line":269},[137,57545,57546],{"class":284},"    You are a witty tech podcast writer.\n",[137,57548,57549],{"class":139,"line":278},[137,57550,57551],{"class":284},"    Convert markdown into a conversation between Aleks and Nicole.\n",[137,57553,57554],{"class":139,"line":291},[137,57555,57556],{"class":284},"    Keep it educational, funny, and natural.\n",[137,57558,57559],{"class":139,"line":297},[137,57560,57561],{"class":284},"    Never read code verbatim — explain it.\n",[137,57563,57564],{"class":139,"line":302},[137,57565,57566],{"class":284},"    Include intro\u002Foutro and use emotional markers sparingly.\n",[137,57568,57569],{"class":139,"line":662},[137,57570,57571],{"class":284},"    Return JSON with intro, dialogue[], and outro.\n",[137,57573,57574,57576],{"class":139,"line":667},[137,57575,57484],{"class":284},[137,57577,1961],{"class":157},[137,57579,57580,57582,57584],{"class":139,"line":786},[137,57581,57491],{"class":157},[137,57583,57494],{"class":284},[137,57585,1961],{"class":157},[137,57587,57588],{"class":139,"line":798},[137,57589,5422],{"class":157},[123,57591,57593],{"id":57592},"the-editor","The Editor",[27,57595,57596,57597,57600],{},"Even the best AI scriptwriters need a good editor. This agent makes sure everything flows naturally and sounds like something ",[30,57598,57599],{},"actual humans"," would say in a real conversation:",[128,57602,57604],{"className":13299,"code":57603,"language":13301,"meta":133,"style":133},"const editorDual = new Agent({\n    name: \"EditorDual\",\n    instructions: `\n    You are a professional podcast editor.\n    Make the dialogue natural and balanced.\n    Keep voices distinct and add light humor.\n    Return only JSON with the same structure.\n  `,\n    model: \"gpt-4o\",\n});\n",[22,57605,57606,57621,57630,57636,57641,57646,57651,57656,57662,57670],{"__ignoreMap":133},[137,57607,57608,57610,57613,57615,57617,57619],{"class":139,"line":140},[137,57609,3077],{"class":143},[137,57611,57612],{"class":364}," editorDual",[137,57614,151],{"class":143},[137,57616,1426],{"class":143},[137,57618,57420],{"class":147},[137,57620,3175],{"class":157},[137,57622,57623,57625,57628],{"class":139,"line":173},[137,57624,57427],{"class":157},[137,57626,57627],{"class":284},"\"EditorDual\"",[137,57629,1961],{"class":157},[137,57631,57632,57634],{"class":139,"line":188},[137,57633,57437],{"class":157},[137,57635,22062],{"class":284},[137,57637,57638],{"class":139,"line":269},[137,57639,57640],{"class":284},"    You are a professional podcast editor.\n",[137,57642,57643],{"class":139,"line":278},[137,57644,57645],{"class":284},"    Make the dialogue natural and balanced.\n",[137,57647,57648],{"class":139,"line":291},[137,57649,57650],{"class":284},"    Keep voices distinct and add light humor.\n",[137,57652,57653],{"class":139,"line":297},[137,57654,57655],{"class":284},"    Return only JSON with the same structure.\n",[137,57657,57658,57660],{"class":139,"line":302},[137,57659,57484],{"class":284},[137,57661,1961],{"class":157},[137,57663,57664,57666,57668],{"class":139,"line":662},[137,57665,57491],{"class":157},[137,57667,57494],{"class":284},[137,57669,1961],{"class":157},[137,57671,57672],{"class":139,"line":667},[137,57673,5422],{"class":157},[123,57675,57677],{"id":57676},"the-voice-generator","The Voice Generator",[27,57679,57680],{},"Once we have a polished script, the final step is giving each host their own distinct voice:",[128,57682,57684],{"className":13299,"code":57683,"language":13301,"meta":133,"style":133},"async function generateVoiceLine(speaker: \"Aleks\" | \"Nicole\", text: string) {\n    const voice = speaker === \"Aleks\" ? \"alloy\" : \"nova\";\n    const response = await client.audio.speech.create({\n        model: \"tts-1-hd\",\n        voice,\n        input: text,\n    });\n    return Buffer.from(await response.arrayBuffer());\n}\n",[22,57685,57686,57718,57746,57763,57773,57778,57783,57787,57808],{"__ignoreMap":133},[137,57687,57688,57690,57692,57695,57697,57700,57702,57704,57706,57708,57710,57712,57714,57716],{"class":139,"line":140},[137,57689,15050],{"class":143},[137,57691,154],{"class":143},[137,57693,57694],{"class":147}," generateVoiceLine",[137,57696,356],{"class":157},[137,57698,57699],{"class":161},"speaker",[137,57701,894],{"class":143},[137,57703,5588],{"class":284},[137,57705,14113],{"class":143},[137,57707,5601],{"class":284},[137,57709,164],{"class":157},[137,57711,5189],{"class":161},[137,57713,894],{"class":143},[137,57715,13630],{"class":364},[137,57717,170],{"class":157},[137,57719,57720,57722,57725,57727,57730,57732,57734,57736,57739,57741,57744],{"class":139,"line":173},[137,57721,4177],{"class":143},[137,57723,57724],{"class":364}," voice",[137,57726,151],{"class":143},[137,57728,57729],{"class":157}," speaker ",[137,57731,5502],{"class":143},[137,57733,5588],{"class":284},[137,57735,26196],{"class":143},[137,57737,57738],{"class":284}," \"alloy\"",[137,57740,26201],{"class":143},[137,57742,57743],{"class":284}," \"nova\"",[137,57745,3276],{"class":157},[137,57747,57748,57750,57752,57754,57756,57759,57761],{"class":139,"line":188},[137,57749,4177],{"class":143},[137,57751,49787],{"class":364},[137,57753,151],{"class":143},[137,57755,15069],{"class":143},[137,57757,57758],{"class":157}," client.audio.speech.",[137,57760,15075],{"class":147},[137,57762,3175],{"class":157},[137,57764,57765,57768,57771],{"class":139,"line":269},[137,57766,57767],{"class":157},"        model: ",[137,57769,57770],{"class":284},"\"tts-1-hd\"",[137,57772,1961],{"class":157},[137,57774,57775],{"class":139,"line":278},[137,57776,57777],{"class":157},"        voice,\n",[137,57779,57780],{"class":139,"line":291},[137,57781,57782],{"class":157},"        input: text,\n",[137,57784,57785],{"class":139,"line":297},[137,57786,2832],{"class":157},[137,57788,57789,57791,57794,57796,57798,57800,57803,57806],{"class":139,"line":302},[137,57790,176],{"class":143},[137,57792,57793],{"class":157}," Buffer.",[137,57795,10954],{"class":147},[137,57797,356],{"class":157},[137,57799,54992],{"class":143},[137,57801,57802],{"class":157}," response.",[137,57804,57805],{"class":147},"arrayBuffer",[137,57807,14173],{"class":157},[137,57809,57810],{"class":139,"line":662},[137,57811,510],{"class":157},[27,57813,57814,57815,57818],{},"I'm using ",[42,57816,57817],{},"OpenAI's high-definition text-to-speech"," model here, which produces surprisingly natural audio - it sounds more like a studio recording than a robotic voice assistant.",[104,57820,49826],{"id":49825},[27,57822,57823],{},"Now that we have our agents and voice generator ready, here's how I orchestrate the whole workflow:",[2569,57825,57826,57829,57832,57839,57842],{},[1006,57827,57828],{},"Generate the first draft of the script using either the single or dual-host scriptwriter.",[1006,57830,57831],{},"Pass it through the editor agent to refine the pacing and flow.",[1006,57833,57834,57835,57838],{},"Parse the JSON output and save it to ",[22,57836,57837],{},"podcast_script.json"," so you can review it if needed.",[1006,57840,57841],{},"Convert each line of the script to audio, one segment at a time.",[1006,57843,57844,57845,1017],{},"Merge all the audio segments and save the final ",[22,57846,57240],{},[27,57848,57849],{},"Here's the main function that handles everything:",[128,57851,57853],{"className":13299,"code":57852,"language":13301,"meta":133,"style":133},"import fs from \"fs\";\n\nasync function generatePodcastFromMarkdown(\n    markdown: string,\n    outputFileName = \"podcast_episode.mp3\",\n    speakers: 1 | 2 = 1\n) {\n    const draft = await run(\n        speakers === 1 ? scriptwriterSingle : scriptwriterDual,\n        `Convert this markdown tech article into a podcast script:\\n\\n${markdown}`\n    );\n    const draftText = extractAllTextOutput(draft.newItems);\n\n    const edited = await run(\n        speakers === 1 ? editorSingle : editorDual,\n        `Please edit and polish this podcast script:\\n\\n${draftText}`\n    );\n    let scriptText = extractAllTextOutput(edited.newItems);\n\n    const match = scriptText.match(\u002F```(?:json)?\\s*(\\{[\\s\\S]*\\})\\s*```\u002F);\n    if (match) scriptText = match[1];\n\n    const script = JSON.parse(scriptText) as PodcastScript;\n    fs.writeFileSync(\"podcast_script.json\", JSON.stringify(script, null, 2));\n\n    const chunks: Buffer[] = [];\n\n    if (speakers === 1) {\n        const solo = script as PodcastScriptSingle;\n        chunks.push(await generateVoiceLine(\"Aleks\", solo.intro));\n        chunks.push(await generateVoiceLine(\"Aleks\", solo.content));\n        chunks.push(await generateVoiceLine(\"Aleks\", solo.outro));\n    } else {\n        const duo = script as PodcastScriptDual;\n        chunks.push(await generateVoiceLine(\"Aleks\", duo.intro));\n        for (const line of duo.dialogue) {\n            chunks.push(await generateVoiceLine(line.speaker, line.text));\n        }\n        chunks.push(await generateVoiceLine(\"Nicole\", duo.outro));\n    }\n\n    fs.writeFileSync(outputFileName, Buffer.concat(chunks));\n}\n",[22,57854,57855,57867,57871,57882,57893,57905,57923,57927,57942,57961,57976,57980,57995,57999,58014,58032,58046,58050,58065,58069,58125,58141,58145,58170,58202,58206,58225,58229,58242,58261,58281,58300,58319,58327,58345,58364,58381,58397,58401,58420,58424,58428,58443],{"__ignoreMap":133},[137,57856,57857,57859,57861,57863,57865],{"class":139,"line":140},[137,57858,10287],{"class":143},[137,57860,48449],{"class":157},[137,57862,10954],{"class":143},[137,57864,48454],{"class":284},[137,57866,3276],{"class":157},[137,57868,57869],{"class":139,"line":173},[137,57870,516],{"emptyLinePlaceholder":515},[137,57872,57873,57875,57877,57880],{"class":139,"line":188},[137,57874,15050],{"class":143},[137,57876,154],{"class":143},[137,57878,57879],{"class":147}," generatePodcastFromMarkdown",[137,57881,11813],{"class":157},[137,57883,57884,57887,57889,57891],{"class":139,"line":269},[137,57885,57886],{"class":161},"    markdown",[137,57888,894],{"class":143},[137,57890,13630],{"class":364},[137,57892,1961],{"class":157},[137,57894,57895,57898,57900,57903],{"class":139,"line":278},[137,57896,57897],{"class":161},"    outputFileName",[137,57899,151],{"class":143},[137,57901,57902],{"class":284}," \"podcast_episode.mp3\"",[137,57904,1961],{"class":157},[137,57906,57907,57910,57912,57914,57916,57918,57920],{"class":139,"line":291},[137,57908,57909],{"class":161},"    speakers",[137,57911,894],{"class":143},[137,57913,8030],{"class":364},[137,57915,14113],{"class":143},[137,57917,40783],{"class":364},[137,57919,151],{"class":143},[137,57921,57922],{"class":364}," 1\n",[137,57924,57925],{"class":139,"line":297},[137,57926,170],{"class":157},[137,57928,57929,57931,57934,57936,57938,57940],{"class":139,"line":302},[137,57930,4177],{"class":143},[137,57932,57933],{"class":364}," draft",[137,57935,151],{"class":143},[137,57937,15069],{"class":143},[137,57939,9578],{"class":147},[137,57941,11813],{"class":157},[137,57943,57944,57947,57949,57951,57953,57956,57958],{"class":139,"line":662},[137,57945,57946],{"class":157},"        speakers ",[137,57948,5502],{"class":143},[137,57950,8030],{"class":364},[137,57952,26196],{"class":143},[137,57954,57955],{"class":157}," scriptwriterSingle ",[137,57957,894],{"class":143},[137,57959,57960],{"class":157}," scriptwriterDual,\n",[137,57962,57963,57966,57968,57970,57973],{"class":139,"line":667},[137,57964,57965],{"class":284},"        `Convert this markdown tech article into a podcast script:",[137,57967,50066],{"class":364},[137,57969,49545],{"class":284},[137,57971,57972],{"class":157},"markdown",[137,57974,57975],{"class":284},"}`\n",[137,57977,57978],{"class":139,"line":786},[137,57979,11875],{"class":157},[137,57981,57982,57984,57987,57989,57992],{"class":139,"line":798},[137,57983,4177],{"class":143},[137,57985,57986],{"class":364}," draftText",[137,57988,151],{"class":143},[137,57990,57991],{"class":147}," extractAllTextOutput",[137,57993,57994],{"class":157},"(draft.newItems);\n",[137,57996,57997],{"class":139,"line":803},[137,57998,516],{"emptyLinePlaceholder":515},[137,58000,58001,58003,58006,58008,58010,58012],{"class":139,"line":931},[137,58002,4177],{"class":143},[137,58004,58005],{"class":364}," edited",[137,58007,151],{"class":143},[137,58009,15069],{"class":143},[137,58011,9578],{"class":147},[137,58013,11813],{"class":157},[137,58015,58016,58018,58020,58022,58024,58027,58029],{"class":139,"line":1568},[137,58017,57946],{"class":157},[137,58019,5502],{"class":143},[137,58021,8030],{"class":364},[137,58023,26196],{"class":143},[137,58025,58026],{"class":157}," editorSingle ",[137,58028,894],{"class":143},[137,58030,58031],{"class":157}," editorDual,\n",[137,58033,58034,58037,58039,58041,58044],{"class":139,"line":1573},[137,58035,58036],{"class":284},"        `Please edit and polish this podcast script:",[137,58038,50066],{"class":364},[137,58040,49545],{"class":284},[137,58042,58043],{"class":157},"draftText",[137,58045,57975],{"class":284},[137,58047,58048],{"class":139,"line":1578},[137,58049,11875],{"class":157},[137,58051,58052,58055,58058,58060,58062],{"class":139,"line":1588},[137,58053,58054],{"class":143},"    let",[137,58056,58057],{"class":157}," scriptText ",[137,58059,253],{"class":143},[137,58061,57991],{"class":147},[137,58063,58064],{"class":157},"(edited.newItems);\n",[137,58066,58067],{"class":139,"line":1601},[137,58068,516],{"emptyLinePlaceholder":515},[137,58070,58071,58073,58076,58078,58081,58084,58086,58088,58091,58093,58096,58098,58100,58104,58107,58109,58112,58114,58116,58118,58121,58123],{"class":139,"line":3802},[137,58072,4177],{"class":143},[137,58074,58075],{"class":364}," match",[137,58077,151],{"class":143},[137,58079,58080],{"class":157}," scriptText.",[137,58082,58083],{"class":147},"match",[137,58085,356],{"class":157},[137,58087,47],{"class":284},[137,58089,58090],{"class":14746},"```(?:json)",[137,58092,12972],{"class":143},[137,58094,58095],{"class":364},"\\s",[137,58097,7672],{"class":143},[137,58099,356],{"class":14746},[137,58101,58103],{"class":58102},"snhLl","\\{",[137,58105,58106],{"class":364},"[\\s\\S]",[137,58108,7672],{"class":143},[137,58110,58111],{"class":58102},"\\}",[137,58113,14105],{"class":14746},[137,58115,58095],{"class":364},[137,58117,7672],{"class":143},[137,58119,58120],{"class":14746},"```",[137,58122,47],{"class":284},[137,58124,1502],{"class":157},[137,58126,58127,58129,58132,58134,58137,58139],{"class":139,"line":3808},[137,58128,24696],{"class":143},[137,58130,58131],{"class":157}," (match) scriptText ",[137,58133,253],{"class":143},[137,58135,58136],{"class":157}," match[",[137,58138,6065],{"class":364},[137,58140,5727],{"class":157},[137,58142,58143],{"class":139,"line":3822},[137,58144,516],{"emptyLinePlaceholder":515},[137,58146,58147,58149,58152,58154,58156,58158,58160,58163,58165,58168],{"class":139,"line":3827},[137,58148,4177],{"class":143},[137,58150,58151],{"class":364}," script",[137,58153,151],{"class":143},[137,58155,17436],{"class":364},[137,58157,1017],{"class":157},[137,58159,17441],{"class":147},[137,58161,58162],{"class":157},"(scriptText) ",[137,58164,24431],{"class":143},[137,58166,58167],{"class":147}," PodcastScript",[137,58169,3276],{"class":157},[137,58171,58172,58175,58178,58180,58183,58185,58187,58189,58191,58194,58196,58198,58200],{"class":139,"line":3832},[137,58173,58174],{"class":157},"    fs.",[137,58176,58177],{"class":147},"writeFileSync",[137,58179,356],{"class":157},[137,58181,58182],{"class":284},"\"podcast_script.json\"",[137,58184,164],{"class":157},[137,58186,22554],{"class":364},[137,58188,1017],{"class":157},[137,58190,24816],{"class":147},[137,58192,58193],{"class":157},"(script, ",[137,58195,11700],{"class":364},[137,58197,164],{"class":157},[137,58199,10345],{"class":364},[137,58201,8614],{"class":157},[137,58203,58204],{"class":139,"line":3840},[137,58205,516],{"emptyLinePlaceholder":515},[137,58207,58208,58210,58213,58215,58218,58221,58223],{"class":139,"line":3846},[137,58209,4177],{"class":143},[137,58211,58212],{"class":364}," chunks",[137,58214,894],{"class":143},[137,58216,58217],{"class":147}," Buffer",[137,58219,58220],{"class":157},"[] ",[137,58222,253],{"class":143},[137,58224,8556],{"class":157},[137,58226,58227],{"class":139,"line":3861},[137,58228,516],{"emptyLinePlaceholder":515},[137,58230,58231,58233,58236,58238,58240],{"class":139,"line":3883},[137,58232,24696],{"class":143},[137,58234,58235],{"class":157}," (speakers ",[137,58237,5502],{"class":143},[137,58239,8030],{"class":364},[137,58241,170],{"class":157},[137,58243,58244,58246,58249,58251,58254,58256,58259],{"class":139,"line":3896},[137,58245,3008],{"class":143},[137,58247,58248],{"class":364}," solo",[137,58250,151],{"class":143},[137,58252,58253],{"class":157}," script ",[137,58255,24431],{"class":143},[137,58257,58258],{"class":147}," PodcastScriptSingle",[137,58260,3276],{"class":157},[137,58262,58263,58266,58268,58270,58272,58274,58276,58278],{"class":139,"line":3901},[137,58264,58265],{"class":157},"        chunks.",[137,58267,8583],{"class":147},[137,58269,356],{"class":157},[137,58271,54992],{"class":143},[137,58273,57694],{"class":147},[137,58275,356],{"class":157},[137,58277,1958],{"class":284},[137,58279,58280],{"class":157},", solo.intro));\n",[137,58282,58283,58285,58287,58289,58291,58293,58295,58297],{"class":139,"line":3906},[137,58284,58265],{"class":157},[137,58286,8583],{"class":147},[137,58288,356],{"class":157},[137,58290,54992],{"class":143},[137,58292,57694],{"class":147},[137,58294,356],{"class":157},[137,58296,1958],{"class":284},[137,58298,58299],{"class":157},", solo.content));\n",[137,58301,58302,58304,58306,58308,58310,58312,58314,58316],{"class":139,"line":3911},[137,58303,58265],{"class":157},[137,58305,8583],{"class":147},[137,58307,356],{"class":157},[137,58309,54992],{"class":143},[137,58311,57694],{"class":147},[137,58313,356],{"class":157},[137,58315,1958],{"class":284},[137,58317,58318],{"class":157},", solo.outro));\n",[137,58320,58321,58323,58325],{"class":139,"line":4666},[137,58322,24944],{"class":157},[137,58324,24947],{"class":143},[137,58326,256],{"class":157},[137,58328,58329,58331,58334,58336,58338,58340,58343],{"class":139,"line":4672},[137,58330,3008],{"class":143},[137,58332,58333],{"class":364}," duo",[137,58335,151],{"class":143},[137,58337,58253],{"class":157},[137,58339,24431],{"class":143},[137,58341,58342],{"class":147}," PodcastScriptDual",[137,58344,3276],{"class":157},[137,58346,58347,58349,58351,58353,58355,58357,58359,58361],{"class":139,"line":4680},[137,58348,58265],{"class":157},[137,58350,8583],{"class":147},[137,58352,356],{"class":157},[137,58354,54992],{"class":143},[137,58356,57694],{"class":147},[137,58358,356],{"class":157},[137,58360,1958],{"class":284},[137,58362,58363],{"class":157},", duo.intro));\n",[137,58365,58366,58369,58371,58373,58376,58378],{"class":139,"line":4711},[137,58367,58368],{"class":143},"        for",[137,58370,158],{"class":157},[137,58372,3077],{"class":143},[137,58374,58375],{"class":364}," line",[137,58377,48913],{"class":143},[137,58379,58380],{"class":157}," duo.dialogue) {\n",[137,58382,58383,58386,58388,58390,58392,58394],{"class":139,"line":4716},[137,58384,58385],{"class":157},"            chunks.",[137,58387,8583],{"class":147},[137,58389,356],{"class":157},[137,58391,54992],{"class":143},[137,58393,57694],{"class":147},[137,58395,58396],{"class":157},"(line.speaker, line.text));\n",[137,58398,58399],{"class":139,"line":4721},[137,58400,1966],{"class":157},[137,58402,58403,58405,58407,58409,58411,58413,58415,58417],{"class":139,"line":4727},[137,58404,58265],{"class":157},[137,58406,8583],{"class":147},[137,58408,356],{"class":157},[137,58410,54992],{"class":143},[137,58412,57694],{"class":147},[137,58414,356],{"class":157},[137,58416,33336],{"class":284},[137,58418,58419],{"class":157},", duo.outro));\n",[137,58421,58422],{"class":139,"line":4732},[137,58423,294],{"class":157},[137,58425,58426],{"class":139,"line":5006},[137,58427,516],{"emptyLinePlaceholder":515},[137,58429,58430,58432,58434,58437,58440],{"class":139,"line":5014},[137,58431,58174],{"class":157},[137,58433,58177],{"class":147},[137,58435,58436],{"class":157},"(outputFileName, Buffer.",[137,58438,58439],{"class":147},"concat",[137,58441,58442],{"class":157},"(chunks));\n",[137,58444,58445],{"class":139,"line":14343},[137,58446,510],{"class":157},[27,58448,58449],{},"And here's the CLI wrapper that makes it easy to use from the command line:",[128,58451,58453],{"className":13299,"code":58452,"language":13301,"meta":133,"style":133},"async function main() {\n    const args = process.argv.slice(2);\n    if (args.length === 0) {\n        console.error(`Usage:\n  npm start \u003Cmarkdown-file> [output-file] [--speakers=1|2]`);\n        process.exit(1);\n    }\n\n    const markdownFile = args[0];\n    if (!fs.existsSync(markdownFile)) {\n        console.error(`❌ File not found: ${markdownFile}`);\n        process.exit(1);\n    }\n\n    const markdown = fs.readFileSync(markdownFile, \"utf-8\");\n\n    \u002F\u002F Check for --speakers flag first\n    let speakers: 1 | 2 = 1; \u002F\u002F Default to single speaker\n    const speakersFlag = args.find((arg) => arg.startsWith(\"--speakers=\"));\n\n    if (speakersFlag) {\n        const speakersValue = speakersFlag.split(\"=\")[1];\n        if (speakersValue === \"2\") {\n            speakers = 2;\n        } else if (speakersValue === \"1\") {\n            speakers = 1;\n        } else {\n            console.error(`❌ Invalid --speakers value. Use --speakers=1 or --speakers=2`);\n            process.exit(1);\n        }\n    }\n\n    \u002F\u002F Get output file (args[1] if it's not the speakers flag)\n    let outputFile = \"podcast_episode.mp3\";\n    if (args[1] && !args[1].startsWith(\"--speakers=\")) {\n        outputFile = args[1];\n    }\n\n    await generatePodcastFromMarkdown(markdown, outputFile, speakers);\n}\n\nmain().catch((error) => {\n    console.error(\"❌ Error:\", error.message);\n    process.exit(1);\n});\n",[22,58454,58455,58466,58486,58502,58513,58520,58534,58538,58542,58558,58574,58592,58604,58608,58612,58633,58637,58642,58666,58702,58706,58713,58738,58752,58763,58781,58791,58799,58812,58825,58829,58833,58837,58842,58855,58885,58898,58902,58906,58915,58919,58923,58942,58956,58969],{"__ignoreMap":133},[137,58456,58457,58459,58461,58464],{"class":139,"line":140},[137,58458,15050],{"class":143},[137,58460,154],{"class":143},[137,58462,58463],{"class":147}," main",[137,58465,275],{"class":157},[137,58467,58468,58470,58473,58475,58478,58480,58482,58484],{"class":139,"line":173},[137,58469,4177],{"class":143},[137,58471,58472],{"class":364}," args",[137,58474,151],{"class":143},[137,58476,58477],{"class":157}," process.argv.",[137,58479,6060],{"class":147},[137,58481,356],{"class":157},[137,58483,10345],{"class":364},[137,58485,1502],{"class":157},[137,58487,58488,58490,58493,58495,58498,58500],{"class":139,"line":188},[137,58489,24696],{"class":143},[137,58491,58492],{"class":157}," (args.",[137,58494,8611],{"class":364},[137,58496,58497],{"class":143}," ===",[137,58499,7687],{"class":364},[137,58501,170],{"class":157},[137,58503,58504,58506,58508,58510],{"class":139,"line":269},[137,58505,350],{"class":157},[137,58507,2812],{"class":147},[137,58509,356],{"class":157},[137,58511,58512],{"class":284},"`Usage:\n",[137,58514,58515,58518],{"class":139,"line":278},[137,58516,58517],{"class":284},"  npm start \u003Cmarkdown-file> [output-file] [--speakers=1|2]`",[137,58519,1502],{"class":157},[137,58521,58522,58525,58528,58530,58532],{"class":139,"line":291},[137,58523,58524],{"class":157},"        process.",[137,58526,58527],{"class":147},"exit",[137,58529,356],{"class":157},[137,58531,6065],{"class":364},[137,58533,1502],{"class":157},[137,58535,58536],{"class":139,"line":297},[137,58537,294],{"class":157},[137,58539,58540],{"class":139,"line":302},[137,58541,516],{"emptyLinePlaceholder":515},[137,58543,58544,58546,58549,58551,58554,58556],{"class":139,"line":662},[137,58545,4177],{"class":143},[137,58547,58548],{"class":364}," markdownFile",[137,58550,151],{"class":143},[137,58552,58553],{"class":157}," args[",[137,58555,6044],{"class":364},[137,58557,5727],{"class":157},[137,58559,58560,58562,58564,58566,58569,58571],{"class":139,"line":667},[137,58561,24696],{"class":143},[137,58563,158],{"class":157},[137,58565,17393],{"class":143},[137,58567,58568],{"class":157},"fs.",[137,58570,48622],{"class":147},[137,58572,58573],{"class":157},"(markdownFile)) {\n",[137,58575,58576,58578,58580,58582,58585,58588,58590],{"class":139,"line":786},[137,58577,350],{"class":157},[137,58579,2812],{"class":147},[137,58581,356],{"class":157},[137,58583,58584],{"class":284},"`❌ File not found: ${",[137,58586,58587],{"class":157},"markdownFile",[137,58589,4706],{"class":284},[137,58591,1502],{"class":157},[137,58593,58594,58596,58598,58600,58602],{"class":139,"line":798},[137,58595,58524],{"class":157},[137,58597,58527],{"class":147},[137,58599,356],{"class":157},[137,58601,6065],{"class":364},[137,58603,1502],{"class":157},[137,58605,58606],{"class":139,"line":803},[137,58607,294],{"class":157},[137,58609,58610],{"class":139,"line":931},[137,58611,516],{"emptyLinePlaceholder":515},[137,58613,58614,58616,58619,58621,58624,58626,58629,58631],{"class":139,"line":1568},[137,58615,4177],{"class":143},[137,58617,58618],{"class":364}," markdown",[137,58620,151],{"class":143},[137,58622,58623],{"class":157}," fs.",[137,58625,48585],{"class":147},[137,58627,58628],{"class":157},"(markdownFile, ",[137,58630,48601],{"class":284},[137,58632,1502],{"class":157},[137,58634,58635],{"class":139,"line":1573},[137,58636,516],{"emptyLinePlaceholder":515},[137,58638,58639],{"class":139,"line":1578},[137,58640,58641],{"class":308},"    \u002F\u002F Check for --speakers flag first\n",[137,58643,58644,58646,58649,58651,58653,58655,58657,58659,58661,58663],{"class":139,"line":1588},[137,58645,58054],{"class":143},[137,58647,58648],{"class":157}," speakers",[137,58650,894],{"class":143},[137,58652,8030],{"class":364},[137,58654,14113],{"class":143},[137,58656,40783],{"class":364},[137,58658,151],{"class":143},[137,58660,8030],{"class":364},[137,58662,2323],{"class":157},[137,58664,58665],{"class":308},"\u002F\u002F Default to single speaker\n",[137,58667,58668,58670,58673,58675,58678,58680,58682,58685,58687,58689,58692,58695,58697,58700],{"class":139,"line":1601},[137,58669,4177],{"class":143},[137,58671,58672],{"class":364}," speakersFlag",[137,58674,151],{"class":143},[137,58676,58677],{"class":157}," args.",[137,58679,32346],{"class":147},[137,58681,2774],{"class":157},[137,58683,58684],{"class":161},"arg",[137,58686,219],{"class":157},[137,58688,222],{"class":143},[137,58690,58691],{"class":157}," arg.",[137,58693,58694],{"class":147},"startsWith",[137,58696,356],{"class":157},[137,58698,58699],{"class":284},"\"--speakers=\"",[137,58701,8614],{"class":157},[137,58703,58704],{"class":139,"line":3802},[137,58705,516],{"emptyLinePlaceholder":515},[137,58707,58708,58710],{"class":139,"line":3808},[137,58709,24696],{"class":143},[137,58711,58712],{"class":157}," (speakersFlag) {\n",[137,58714,58715,58717,58720,58722,58725,58727,58729,58732,58734,58736],{"class":139,"line":3822},[137,58716,3008],{"class":143},[137,58718,58719],{"class":364}," speakersValue",[137,58721,151],{"class":143},[137,58723,58724],{"class":157}," speakersFlag.",[137,58726,8537],{"class":147},[137,58728,356],{"class":157},[137,58730,58731],{"class":284},"\"=\"",[137,58733,14224],{"class":157},[137,58735,6065],{"class":364},[137,58737,5727],{"class":157},[137,58739,58740,58742,58745,58747,58750],{"class":139,"line":3827},[137,58741,5496],{"class":143},[137,58743,58744],{"class":157}," (speakersValue ",[137,58746,5502],{"class":143},[137,58748,58749],{"class":284}," \"2\"",[137,58751,170],{"class":157},[137,58753,58754,58757,58759,58761],{"class":139,"line":3832},[137,58755,58756],{"class":157},"            speakers ",[137,58758,253],{"class":143},[137,58760,40783],{"class":364},[137,58762,3276],{"class":157},[137,58764,58765,58767,58769,58772,58774,58776,58779],{"class":139,"line":3840},[137,58766,15729],{"class":157},[137,58768,24947],{"class":143},[137,58770,58771],{"class":143}," if",[137,58773,58744],{"class":157},[137,58775,5502],{"class":143},[137,58777,58778],{"class":284}," \"1\"",[137,58780,170],{"class":157},[137,58782,58783,58785,58787,58789],{"class":139,"line":3846},[137,58784,58756],{"class":157},[137,58786,253],{"class":143},[137,58788,8030],{"class":364},[137,58790,3276],{"class":157},[137,58792,58793,58795,58797],{"class":139,"line":3861},[137,58794,15729],{"class":157},[137,58796,24947],{"class":143},[137,58798,256],{"class":157},[137,58800,58801,58803,58805,58807,58810],{"class":139,"line":3883},[137,58802,1493],{"class":157},[137,58804,2812],{"class":147},[137,58806,356],{"class":157},[137,58808,58809],{"class":284},"`❌ Invalid --speakers value. Use --speakers=1 or --speakers=2`",[137,58811,1502],{"class":157},[137,58813,58814,58817,58819,58821,58823],{"class":139,"line":3896},[137,58815,58816],{"class":157},"            process.",[137,58818,58527],{"class":147},[137,58820,356],{"class":157},[137,58822,6065],{"class":364},[137,58824,1502],{"class":157},[137,58826,58827],{"class":139,"line":3901},[137,58828,1966],{"class":157},[137,58830,58831],{"class":139,"line":3906},[137,58832,294],{"class":157},[137,58834,58835],{"class":139,"line":3911},[137,58836,516],{"emptyLinePlaceholder":515},[137,58838,58839],{"class":139,"line":4666},[137,58840,58841],{"class":308},"    \u002F\u002F Get output file (args[1] if it's not the speakers flag)\n",[137,58843,58844,58846,58849,58851,58853],{"class":139,"line":4672},[137,58845,58054],{"class":143},[137,58847,58848],{"class":157}," outputFile ",[137,58850,253],{"class":143},[137,58852,57902],{"class":284},[137,58854,3276],{"class":157},[137,58856,58857,58859,58862,58864,58866,58868,58870,58873,58875,58877,58879,58881,58883],{"class":139,"line":4680},[137,58858,24696],{"class":143},[137,58860,58861],{"class":157}," (args[",[137,58863,6065],{"class":364},[137,58865,5796],{"class":157},[137,58867,3351],{"class":143},[137,58869,27133],{"class":143},[137,58871,58872],{"class":157},"args[",[137,58874,6065],{"class":364},[137,58876,6047],{"class":157},[137,58878,58694],{"class":147},[137,58880,356],{"class":157},[137,58882,58699],{"class":284},[137,58884,34157],{"class":157},[137,58886,58887,58890,58892,58894,58896],{"class":139,"line":4711},[137,58888,58889],{"class":157},"        outputFile ",[137,58891,253],{"class":143},[137,58893,58553],{"class":157},[137,58895,6065],{"class":364},[137,58897,5727],{"class":157},[137,58899,58900],{"class":139,"line":4716},[137,58901,294],{"class":157},[137,58903,58904],{"class":139,"line":4721},[137,58905,516],{"emptyLinePlaceholder":515},[137,58907,58908,58910,58912],{"class":139,"line":4727},[137,58909,15100],{"class":143},[137,58911,57879],{"class":147},[137,58913,58914],{"class":157},"(markdown, outputFile, speakers);\n",[137,58916,58917],{"class":139,"line":4732},[137,58918,510],{"class":157},[137,58920,58921],{"class":139,"line":5006},[137,58922,516],{"emptyLinePlaceholder":515},[137,58924,58925,58928,58930,58932,58934,58936,58938,58940],{"class":139,"line":5014},[137,58926,58927],{"class":147},"main",[137,58929,17766],{"class":157},[137,58931,2807],{"class":147},[137,58933,2774],{"class":157},[137,58935,2812],{"class":161},[137,58937,219],{"class":157},[137,58939,222],{"class":143},[137,58941,256],{"class":157},[137,58943,58944,58946,58948,58950,58953],{"class":139,"line":14343},[137,58945,493],{"class":157},[137,58947,2812],{"class":147},[137,58949,356],{"class":157},[137,58951,58952],{"class":284},"\"❌ Error:\"",[137,58954,58955],{"class":157},", error.message);\n",[137,58957,58958,58961,58963,58965,58967],{"class":139,"line":24199},[137,58959,58960],{"class":157},"    process.",[137,58962,58527],{"class":147},[137,58964,356],{"class":157},[137,58966,6065],{"class":364},[137,58968,1502],{"class":157},[137,58970,58971],{"class":139,"line":24773},[137,58972,5422],{"class":157},[104,58974,58976],{"id":58975},"running-the-generator","Running the Generator",[27,58978,58979],{},"Once you've got everything set up, using it is super straightforward:",[128,58981,58983],{"className":8665,"code":58982,"language":8667,"meta":133,"style":133},"npm start -- \u003Cmarkdown-file> [output-file] [--speakers=1|2]\n",[22,58984,58985],{"__ignoreMap":133},[137,58986,58987,58989,58992,58995,58997,59000,59002,59004,59007,59009,59011,59013],{"class":139,"line":140},[137,58988,9536],{"class":147},[137,58990,58991],{"class":284}," start",[137,58993,58994],{"class":364}," --",[137,58996,29304],{"class":143},[137,58998,58999],{"class":284},"markdown-fil",[137,59001,24534],{"class":157},[137,59003,30029],{"class":143},[137,59005,59006],{"class":157}," [output-file] [--speakers",[137,59008,253],{"class":143},[137,59010,6065],{"class":157},[137,59012,7684],{"class":143},[137,59014,59015],{"class":157},"2]\n",[27,59017,59018],{},"Here are a couple of examples:",[128,59020,59022],{"className":8665,"code":59021,"language":8667,"meta":133,"style":133},"npm start -- .\u002Farticle.md\nnpm start -- .\u002Farticle.md .\u002Fmy-podcast.mp3 --speakers=2\n",[22,59023,59024,59035],{"__ignoreMap":133},[137,59025,59026,59028,59030,59032],{"class":139,"line":140},[137,59027,9536],{"class":147},[137,59029,58991],{"class":284},[137,59031,58994],{"class":364},[137,59033,59034],{"class":284}," .\u002Farticle.md\n",[137,59036,59037,59039,59041,59043,59046,59049],{"class":139,"line":173},[137,59038,9536],{"class":147},[137,59040,58991],{"class":284},[137,59042,58994],{"class":364},[137,59044,59045],{"class":284}," .\u002Farticle.md",[137,59047,59048],{"class":284}," .\u002Fmy-podcast.mp3",[137,59050,59051],{"class":364}," --speakers=2\n",[1003,59053,59054,59057,59067],{},[1006,59055,59056],{},"If you forget to include a file, you'll get a helpful usage message.",[1006,59058,59059,59060,59063,59064,4409],{},"The default is ",[42,59061,59062],{},"single host"," mode (",[22,59065,59066],{},"--speakers=1",[1006,59068,59069,59070,1017],{},"The output defaults to ",[22,59071,57240],{},[104,59073,59075],{"id":59074},"wrapping-up","Wrapping Up",[27,59077,59078],{},"With some TypeScript and OpenAI's Agents SDK, I've completely automated my podcast production workflow. And you can too.",[27,59080,59081],{},"But here's what really excites me about this: it's not just about saving time - it's about opening up new creative possibilities. You can fine-tune the tone for different topics, create distinct host personalities for different blog categories, or experiment with pacing and style. The line between written and spoken content is disappearing, and tools like this make it easy to be on both sides at once.",[27,59083,59084,59085,59088],{},"If your blog already tells great stories in text, now you can make it ",[30,59086,59087],{},"tell"," those stories out loud too.",[27,59090,59091,59092,59096],{},"You can find the complete code in the following GitHub ",[45,59093,2726],{"href":59094,"target":2716,"rel":59095},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fblog-article-to-podcast",[2718,2719],". Feel free to clone and explore it.",[2617,59098,59099],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":133,"searchDepth":173,"depth":173,"links":59101},[59102,59103,59104,59105,59106,59111,59112,59113],{"id":57089,"depth":173,"text":57090},{"id":57168,"depth":173,"text":57169},{"id":57207,"depth":173,"text":57208},{"id":57257,"depth":173,"text":57258},{"id":57373,"depth":173,"text":57374,"children":59107},[59108,59109,59110],{"id":57380,"depth":188,"text":57381},{"id":57592,"depth":188,"text":57593},{"id":57676,"depth":188,"text":57677},{"id":49825,"depth":173,"text":49826},{"id":58975,"depth":173,"text":58976},{"id":59074,"depth":173,"text":59075},"Transform your Markdown blog posts into engaging, conversational podcasts using OpenAI’s Agents SDK. Discover how to automate every step - from scriptwriting and editing to voice generation - to produce natural-sounding MP3 episodes in minutes. Build your own AI-powered production pipeline and give your blog a real voice, no studio or audio expertise required.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1762858276\u002Fblog\u002Fturning-markdown-into-podcasts-with-openai-agents\u002Fturning-markdown-into-podcasts-with-openai-agents_aav3tt",[59117,59118,59119,59120,59121,59122,59123,59124,59125,59126,59127,59128],"OpenAI Agents SDK","AI podcast generation","Markdown to podcast","text to speech","AI workflow automation","GPT-4o","TTS-1-HD","AI content creation","podcast automation","developer tools","blog to audio","OpenAI tutorials",{},"\u002F2025\u002F11\u002F13\u002Fturning-markdown-into-podcasts-with-openai-agents","13th November 2025",{"title":57043,"description":59114},"2025\u002F11\u002F13\u002Fturning-markdown-into-podcasts-with-openai-agents","iYxH7ua6jk4Z_EZBEhI-SwU5Xq4X03s977R__X7BuLI",{"id":59136,"title":59137,"articleTags":59138,"author":11,"blog":12,"body":59139,"description":61065,"extension":2649,"image":61066,"keywords":61067,"meta":61080,"navigation":515,"path":61081,"published":61082,"readTime":188,"seo":61083,"stem":61084,"type":2662,"__hash__":61085},"content\u002F2025\u002F11\u002F16\u002Fmake-your-audio-play-with-real-time-transcript-highlighting.md","Make Your Audio Play with Real-Time Transcript Highlighting",[9,7531,10],{"type":14,"value":59140,"toc":61053},[59141,59144,59158,59160,59164,59169,59175,59178,59182,59185,59202,59204,59207,59215,59219,59222,59351,59354,59372,59381,59385,59390,59686,59689,59712,59716,59723,59890,59893,59930,59934,59940,60150,60153,60189,60193,60196,60335,60338,60355,60359,60366,60965,60968,60989,60993,60996,61037,61039,61042,61050],[17,59142,59137],{"id":59143},"make-your-audio-play-with-real-time-transcript-highlighting",[27,59145,59146],{},[30,59147,59148,36,59150,40,59152],{},[33,59149],{"value":35},[33,59151],{"value":39},[42,59153,59154],{},[45,59155,59156],{"href":47},[33,59157],{"value":50},[52,59159],{":tags":54},[56,59161],{":audio-src":59162,":transcript-src":59163},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F11\u002F16\u002Fmake-your-audio-play-with-real-time-transcript-highlighting\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F11\u002F16\u002Fmake-your-audio-play-with-real-time-transcript-highlighting\u002Fsummary.json",[27,59165,59166],{},[63,59167],{"alt":12847,"src":59168},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_850,e_sharpen:100\u002Fv1763271005\u002Fblog\u002Fmake-your-audio-play-with-real-time-transcript-highlighting\u002Fmake-your-audio-play-with-real-time-transcript-highlighting_hgmjsl",[27,59170,59171,59172,1017],{},"This blog article is a continuation of my previous post where I showed you how to convert markdown into podcasts using OpenAI agents. If you haven't read that one yet, you can ",[47718,59173,59174],{"to":59130},"check it out here",[27,59176,59177],{},"In this article, I'm going to show you how to take that generated MP3 podcast-style audio from markdown and create an interactive audio player with live captions. Think of it like YouTube's live captions feature, where the text highlights in sync with what's being said. We'll build this using just HTML, CSS, and JavaScript.",[104,59179,59181],{"id":59180},"what-were-building","What We're Building",[27,59183,59184],{},"Here's what our audio player will do:",[1003,59186,59187,59190,59193,59196,59199],{},[1006,59188,59189],{},"Play your MP3 audio file (the one we generated from markdown in my previous article)",[1006,59191,59192],{},"Display the full transcript below the player",[1006,59194,59195],{},"Highlight each line of text as it's being spoken",[1006,59197,59198],{},"Automatically scroll to keep the active text in view",[1006,59200,59201],{},"Let listeners click any line to jump to that part of the audio",[104,59203,57169],{"id":57168},[27,59205,59206],{},"Before we dive in, make sure you have:",[2569,59208,59209,59212],{},[1006,59210,59211],{},"An audio file (MP3, WAV, or any browser-supported format)",[1006,59213,59214],{},"A transcript with timestamps in JSON format (don't worry if you don't have this yet - I'll show you how to get it in the next section)",[104,59216,59218],{"id":59217},"understanding-the-transcript-format","Understanding the Transcript Format",[27,59220,59221],{},"The key to making everything work smoothly is having a transcript with precise timestamps. Here's what the JSON format looks like:",[128,59223,59225],{"className":5155,"code":59224,"language":5157,"meta":133,"style":133},"[\n    {\n        \"start\": 0,\n        \"end\": 3.28,\n        \"text\": \"Welcome back to the podcast where tech meets creativity.\"\n    },\n    {\n        \"start\": 3.28,\n        \"end\": 5.04,\n        \"text\": \"Today, we're diving into how to transform\"\n    },\n    {\n        \"start\": 5.04,\n        \"end\": 7.72,\n        \"text\": \"your markdown blogs into engaging podcasts\"\n    }\n]\n",[22,59226,59227,59231,59235,59245,59257,59267,59271,59275,59285,59296,59305,59309,59313,59323,59334,59343,59347],{"__ignoreMap":133},[137,59228,59229],{"class":139,"line":140},[137,59230,28046],{"class":157},[137,59232,59233],{"class":139,"line":173},[137,59234,28051],{"class":157},[137,59236,59237,59239,59241,59243],{"class":139,"line":188},[137,59238,57357],{"class":364},[137,59240,726],{"class":157},[137,59242,6044],{"class":364},[137,59244,1961],{"class":157},[137,59246,59247,59250,59252,59255],{"class":139,"line":269},[137,59248,59249],{"class":364},"        \"end\"",[137,59251,726],{"class":157},[137,59253,59254],{"class":364},"3.28",[137,59256,1961],{"class":157},[137,59258,59259,59262,59264],{"class":139,"line":278},[137,59260,59261],{"class":364},"        \"text\"",[137,59263,726],{"class":157},[137,59265,59266],{"class":284},"\"Welcome back to the podcast where tech meets creativity.\"\n",[137,59268,59269],{"class":139,"line":291},[137,59270,775],{"class":157},[137,59272,59273],{"class":139,"line":297},[137,59274,28051],{"class":157},[137,59276,59277,59279,59281,59283],{"class":139,"line":302},[137,59278,57357],{"class":364},[137,59280,726],{"class":157},[137,59282,59254],{"class":364},[137,59284,1961],{"class":157},[137,59286,59287,59289,59291,59294],{"class":139,"line":662},[137,59288,59249],{"class":364},[137,59290,726],{"class":157},[137,59292,59293],{"class":364},"5.04",[137,59295,1961],{"class":157},[137,59297,59298,59300,59302],{"class":139,"line":667},[137,59299,59261],{"class":364},[137,59301,726],{"class":157},[137,59303,59304],{"class":284},"\"Today, we're diving into how to transform\"\n",[137,59306,59307],{"class":139,"line":786},[137,59308,775],{"class":157},[137,59310,59311],{"class":139,"line":798},[137,59312,28051],{"class":157},[137,59314,59315,59317,59319,59321],{"class":139,"line":803},[137,59316,57357],{"class":364},[137,59318,726],{"class":157},[137,59320,59293],{"class":364},[137,59322,1961],{"class":157},[137,59324,59325,59327,59329,59332],{"class":139,"line":931},[137,59326,59249],{"class":364},[137,59328,726],{"class":157},[137,59330,59331],{"class":364},"7.72",[137,59333,1961],{"class":157},[137,59335,59336,59338,59340],{"class":139,"line":1568},[137,59337,59261],{"class":364},[137,59339,726],{"class":157},[137,59341,59342],{"class":284},"\"your markdown blogs into engaging podcasts\"\n",[137,59344,59345],{"class":139,"line":1573},[137,59346,294],{"class":157},[137,59348,59349],{"class":139,"line":1578},[137,59350,33307],{"class":157},[27,59352,59353],{},"Each segment includes:",[1003,59355,59356,59362,59367],{},[1006,59357,59358,59361],{},[22,59359,59360],{},"start",": When this text begins (in seconds)",[1006,59363,59364,59366],{},[22,59365,33017],{},": When this text ends (in seconds)",[1006,59368,59369,59371],{},[22,59370,5189],{},": The actual words spoken",[3244,59373,59374],{},[27,59375,59376,59377,1017],{},"If you don't have timestamps yet, you can use OpenAI's Whisper API to generate them automatically from your audio file. I've created a simple project to help you do exactly that-you can find it at this ",[45,59378,2726],{"href":59379,"target":2716,"rel":59380},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Faudio-files-to-timestamped-transcripts-with-openai-whisper",[2718,2719],[104,59382,59384],{"id":59383},"creating-the-html-structure","Creating the HTML Structure",[27,59386,59387,59388,9772],{},"Let's start by building a simple HTML file. Create an ",[22,59389,23204],{},[128,59391,59393],{"className":4024,"code":59392,"language":4026,"meta":133,"style":133},"\u003C!DOCTYPE html>\n\u003Chtml lang=\"en\">\n    \u003Chead>\n        \u003Cmeta charset=\"UTF-8\" \u002F>\n        \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" \u002F>\n        \u003Ctitle>Audio Synced Transcription\u003C\u002Ftitle>\n        \u003Clink rel=\"stylesheet\" href=\"styles.css\" \u002F>\n    \u003C\u002Fhead>\n    \u003Cbody>\n        \u003Cdiv class=\"container\">\n            \u003Ch1>Podcast: Tech Meets Creativity\u003C\u002Fh1>\n\n            \u003Caudio id=\"podcast\" controls src=\"podcast_episode.mp3\">\u003C\u002Faudio>\n\n            \u003Cdiv class=\"transcript-wrapper\">\n                \u003Ch2>Transcript\u003C\u002Fh2>\n                \u003Cdiv id=\"transcript\" class=\"transcript\">\u003C\u002Fdiv>\n            \u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n\n        \u003Cscript src=\"script.js\">\u003C\u002Fscript>\n    \u003C\u002Fbody>\n\u003C\u002Fhtml>\n",[22,59394,59395,59406,59420,59428,59442,59462,59475,59496,59504,59512,59527,59540,59544,59574,59578,59593,59606,59631,59639,59647,59651,59670,59678],{"__ignoreMap":133},[137,59396,59397,59399,59402,59404],{"class":139,"line":140},[137,59398,25574],{"class":157},[137,59400,59401],{"class":4036},"DOCTYPE",[137,59403,25580],{"class":147},[137,59405,4053],{"class":157},[137,59407,59408,59410,59412,59414,59416,59418],{"class":139,"line":173},[137,59409,4033],{"class":157},[137,59411,4026],{"class":4036},[137,59413,25591],{"class":147},[137,59415,253],{"class":157},[137,59417,25596],{"class":284},[137,59419,4053],{"class":157},[137,59421,59422,59424,59426],{"class":139,"line":188},[137,59423,4072],{"class":157},[137,59425,25605],{"class":4036},[137,59427,4053],{"class":157},[137,59429,59430,59432,59434,59436,59438,59440],{"class":139,"line":269},[137,59431,9826],{"class":157},[137,59433,23508],{"class":4036},[137,59435,25616],{"class":147},[137,59437,253],{"class":157},[137,59439,25621],{"class":284},[137,59441,4078],{"class":157},[137,59443,59444,59446,59448,59450,59452,59454,59456,59458,59460],{"class":139,"line":278},[137,59445,9826],{"class":157},[137,59447,23508],{"class":4036},[137,59449,891],{"class":147},[137,59451,253],{"class":157},[137,59453,25666],{"class":284},[137,59455,25669],{"class":147},[137,59457,253],{"class":157},[137,59459,25674],{"class":284},[137,59461,4078],{"class":157},[137,59463,59464,59466,59468,59471,59473],{"class":139,"line":291},[137,59465,9826],{"class":157},[137,59467,25683],{"class":4036},[137,59469,59470],{"class":157},">Audio Synced Transcription\u003C\u002F",[137,59472,25683],{"class":4036},[137,59474,4053],{"class":157},[137,59476,59477,59479,59481,59483,59485,59487,59489,59491,59494],{"class":139,"line":297},[137,59478,9826],{"class":157},[137,59480,2726],{"class":4036},[137,59482,23226],{"class":147},[137,59484,253],{"class":157},[137,59486,23231],{"class":284},[137,59488,23218],{"class":147},[137,59490,253],{"class":157},[137,59492,59493],{"class":284},"\"styles.css\"",[137,59495,4078],{"class":157},[137,59497,59498,59500,59502],{"class":139,"line":302},[137,59499,8374],{"class":157},[137,59501,25605],{"class":4036},[137,59503,4053],{"class":157},[137,59505,59506,59508,59510],{"class":139,"line":662},[137,59507,4072],{"class":157},[137,59509,4065],{"class":4036},[137,59511,4053],{"class":157},[137,59513,59514,59516,59518,59520,59522,59525],{"class":139,"line":667},[137,59515,9826],{"class":157},[137,59517,8330],{"class":4036},[137,59519,7832],{"class":147},[137,59521,253],{"class":157},[137,59523,59524],{"class":284},"\"container\"",[137,59526,4053],{"class":157},[137,59528,59529,59531,59533,59536,59538],{"class":139,"line":786},[137,59530,23852],{"class":157},[137,59532,17],{"class":4036},[137,59534,59535],{"class":157},">Podcast: Tech Meets Creativity\u003C\u002F",[137,59537,17],{"class":4036},[137,59539,4053],{"class":157},[137,59541,59542],{"class":139,"line":798},[137,59543,516],{"emptyLinePlaceholder":515},[137,59545,59546,59548,59551,59553,59555,59558,59561,59563,59565,59568,59570,59572],{"class":139,"line":803},[137,59547,23852],{"class":157},[137,59549,59550],{"class":4036},"audio",[137,59552,23757],{"class":147},[137,59554,253],{"class":157},[137,59556,59557],{"class":284},"\"podcast\"",[137,59559,59560],{"class":147}," controls",[137,59562,4040],{"class":147},[137,59564,253],{"class":157},[137,59566,59567],{"class":284},"\"podcast_episode.mp3\"",[137,59569,4048],{"class":157},[137,59571,59550],{"class":4036},[137,59573,4053],{"class":157},[137,59575,59576],{"class":139,"line":931},[137,59577,516],{"emptyLinePlaceholder":515},[137,59579,59580,59582,59584,59586,59588,59591],{"class":139,"line":1568},[137,59581,23852],{"class":157},[137,59583,8330],{"class":4036},[137,59585,7832],{"class":147},[137,59587,253],{"class":157},[137,59589,59590],{"class":284},"\"transcript-wrapper\"",[137,59592,4053],{"class":157},[137,59594,59595,59597,59599,59602,59604],{"class":139,"line":1573},[137,59596,23861],{"class":157},[137,59598,104],{"class":4036},[137,59600,59601],{"class":157},">Transcript\u003C\u002F",[137,59603,104],{"class":4036},[137,59605,4053],{"class":157},[137,59607,59608,59610,59612,59614,59616,59619,59621,59623,59625,59627,59629],{"class":139,"line":1578},[137,59609,23861],{"class":157},[137,59611,8330],{"class":4036},[137,59613,23757],{"class":147},[137,59615,253],{"class":157},[137,59617,59618],{"class":284},"\"transcript\"",[137,59620,7832],{"class":147},[137,59622,253],{"class":157},[137,59624,59618],{"class":284},[137,59626,4048],{"class":157},[137,59628,8330],{"class":4036},[137,59630,4053],{"class":157},[137,59632,59633,59635,59637],{"class":139,"line":1588},[137,59634,23980],{"class":157},[137,59636,8330],{"class":4036},[137,59638,4053],{"class":157},[137,59640,59641,59643,59645],{"class":139,"line":1601},[137,59642,9843],{"class":157},[137,59644,8330],{"class":4036},[137,59646,4053],{"class":157},[137,59648,59649],{"class":139,"line":3802},[137,59650,516],{"emptyLinePlaceholder":515},[137,59652,59653,59655,59657,59659,59661,59664,59666,59668],{"class":139,"line":3808},[137,59654,9826],{"class":157},[137,59656,4037],{"class":4036},[137,59658,4040],{"class":147},[137,59660,253],{"class":157},[137,59662,59663],{"class":284},"\"script.js\"",[137,59665,4048],{"class":157},[137,59667,4037],{"class":4036},[137,59669,4053],{"class":157},[137,59671,59672,59674,59676],{"class":139,"line":3822},[137,59673,8374],{"class":157},[137,59675,4065],{"class":4036},[137,59677,4053],{"class":157},[137,59679,59680,59682,59684],{"class":139,"line":3827},[137,59681,4083],{"class":157},[137,59683,4026],{"class":4036},[137,59685,4053],{"class":157},[27,59687,59688],{},"That's all you need! Notice we have:",[1003,59690,59691,59702,59709],{},[1006,59692,59693,59694,59697,59698,59701],{},"An ",[22,59695,59696],{},"\u003Caudio>"," element with ",[22,59699,59700],{},"controls"," so users can play and pause",[1006,59703,59704,59705,59708],{},"An empty ",[22,59706,59707],{},"\u003Cdiv id=\"transcript\">"," where we'll dynamically insert our transcript",[1006,59710,59711],{},"Links to our CSS and JavaScript files",[104,59713,59715],{"id":59714},"loading-and-displaying-the-transcript","Loading and Displaying the Transcript",[27,59717,59718,59719,59722],{},"Now let's create a ",[22,59720,59721],{},"script.js"," file. This is where the magic happens!",[128,59724,59726],{"className":130,"code":59725,"language":132,"meta":133,"style":133},"\u002F\u002F Load transcript data from JSON file\nfetch('podcast_episode_transcript.json')\n  .then(response => response.json())\n  .then(transcriptData => {\n    const transcriptEl = document.getElementById(\"transcript\");\n    const audioEl = document.getElementById(\"podcast\");\n\n    \u002F\u002F Render the transcript with each segment on a new line\n    transcriptEl.innerHTML = transcriptData\n      .map((seg, i) => `\u003Cp class=\"segment\" data-index=\"${i}\">${seg.text}\u003C\u002Fp>`)\n      .join(\"\");\n\n",[22,59727,59728,59733,59745,59767,59782,59801,59820,59824,59829,59839,59878],{"__ignoreMap":133},[137,59729,59730],{"class":139,"line":140},[137,59731,59732],{"class":308},"\u002F\u002F Load transcript data from JSON file\n",[137,59734,59735,59738,59740,59743],{"class":139,"line":173},[137,59736,59737],{"class":147},"fetch",[137,59739,356],{"class":157},[137,59741,59742],{"class":284},"'podcast_episode_transcript.json'",[137,59744,3155],{"class":157},[137,59746,59747,59750,59752,59754,59757,59760,59762,59764],{"class":139,"line":188},[137,59748,59749],{"class":157},"  .",[137,59751,2771],{"class":147},[137,59753,356],{"class":157},[137,59755,59756],{"class":161},"response",[137,59758,59759],{"class":143}," =>",[137,59761,57802],{"class":157},[137,59763,5157],{"class":147},[137,59765,59766],{"class":157},"())\n",[137,59768,59769,59771,59773,59775,59778,59780],{"class":139,"line":269},[137,59770,59749],{"class":157},[137,59772,2771],{"class":147},[137,59774,356],{"class":157},[137,59776,59777],{"class":161},"transcriptData",[137,59779,59759],{"class":143},[137,59781,256],{"class":157},[137,59783,59784,59786,59789,59791,59793,59795,59797,59799],{"class":139,"line":278},[137,59785,4177],{"class":143},[137,59787,59788],{"class":364}," transcriptEl",[137,59790,151],{"class":143},[137,59792,3717],{"class":157},[137,59794,24422],{"class":147},[137,59796,356],{"class":157},[137,59798,59618],{"class":284},[137,59800,1502],{"class":157},[137,59802,59803,59805,59808,59810,59812,59814,59816,59818],{"class":139,"line":291},[137,59804,4177],{"class":143},[137,59806,59807],{"class":364}," audioEl",[137,59809,151],{"class":143},[137,59811,3717],{"class":157},[137,59813,24422],{"class":147},[137,59815,356],{"class":157},[137,59817,59557],{"class":284},[137,59819,1502],{"class":157},[137,59821,59822],{"class":139,"line":297},[137,59823,516],{"emptyLinePlaceholder":515},[137,59825,59826],{"class":139,"line":302},[137,59827,59828],{"class":308},"    \u002F\u002F Render the transcript with each segment on a new line\n",[137,59830,59831,59834,59836],{"class":139,"line":662},[137,59832,59833],{"class":157},"    transcriptEl.innerHTML ",[137,59835,253],{"class":143},[137,59837,59838],{"class":157}," transcriptData\n",[137,59840,59841,59844,59846,59848,59851,59853,59855,59857,59859,59862,59864,59867,59869,59871,59873,59876],{"class":139,"line":667},[137,59842,59843],{"class":157},"      .",[137,59845,37476],{"class":147},[137,59847,2774],{"class":157},[137,59849,59850],{"class":161},"seg",[137,59852,164],{"class":157},[137,59854,53918],{"class":161},[137,59856,219],{"class":157},[137,59858,222],{"class":143},[137,59860,59861],{"class":284}," `\u003Cp class=\"segment\" data-index=\"${",[137,59863,53918],{"class":157},[137,59865,59866],{"class":284},"}\">${",[137,59868,59850],{"class":157},[137,59870,1017],{"class":284},[137,59872,5189],{"class":157},[137,59874,59875],{"class":284},"}\u003C\u002Fp>`",[137,59877,3155],{"class":157},[137,59879,59880,59882,59884,59886,59888],{"class":139,"line":786},[137,59881,59843],{"class":157},[137,59883,8628],{"class":147},[137,59885,356],{"class":157},[137,59887,4535],{"class":284},[137,59889,1502],{"class":157},[27,59891,59892],{},"Let me break down what's happening here:",[2569,59894,59895,59903,59911,59914,59922],{},[1006,59896,59897,59902],{},[42,59898,59899],{},[22,59900,59901],{},"fetch()"," loads our JSON file",[1006,59904,59905,59910],{},[42,59906,59907],{},[22,59908,59909],{},".then(response => response.json())"," converts it to JavaScript data we can work with",[1006,59912,59913],{},"We grab references to our HTML elements",[1006,59915,59916,59921],{},[42,59917,59918],{},[22,59919,59920],{},".map()"," transforms each segment into an HTML paragraph",[1006,59923,59924,59929],{},[42,59925,59926],{},[22,59927,59928],{},"data-index=\"${i}\""," stores each segment's position so we can reference it later",[104,59931,59933],{"id":59932},"syncing-highlights-with-audio-playback","Syncing Highlights with Audio Playback",[27,59935,59936,59937,39740],{},"Add this code inside the ",[22,59938,59939],{},".then()",[128,59941,59943],{"className":130,"code":59942,"language":132,"meta":133,"style":133},"\u002F\u002F Listen to playback\naudioEl.addEventListener(\"timeupdate\", () => {\n    const currentTime = audioEl.currentTime;\n\n    transcriptData.forEach((seg, i) => {\n        const el = transcriptEl.querySelector(`[data-index=\"${i}\"]`);\n\n        if (currentTime >= seg.start && currentTime \u003C= seg.end) {\n            el.classList.add(\"active\");\n            el.scrollIntoView({\n                behavior: \"smooth\",\n                block: \"nearest\",\n                inline: \"center\",\n            });\n        } else {\n            el.classList.remove(\"active\");\n        }\n    });\n});\n",[22,59944,59945,59950,59968,59980,59984,60005,60031,60035,60059,60073,60083,60093,60103,60113,60117,60125,60138,60142,60146],{"__ignoreMap":133},[137,59946,59947],{"class":139,"line":140},[137,59948,59949],{"class":308},"\u002F\u002F Listen to playback\n",[137,59951,59952,59955,59957,59959,59962,59964,59966],{"class":139,"line":173},[137,59953,59954],{"class":157},"audioEl.",[137,59956,4412],{"class":147},[137,59958,356],{"class":157},[137,59960,59961],{"class":284},"\"timeupdate\"",[137,59963,4420],{"class":157},[137,59965,222],{"class":143},[137,59967,256],{"class":157},[137,59969,59970,59972,59975,59977],{"class":139,"line":188},[137,59971,4177],{"class":143},[137,59973,59974],{"class":364}," currentTime",[137,59976,151],{"class":143},[137,59978,59979],{"class":157}," audioEl.currentTime;\n",[137,59981,59982],{"class":139,"line":269},[137,59983,516],{"emptyLinePlaceholder":515},[137,59985,59986,59989,59991,59993,59995,59997,59999,60001,60003],{"class":139,"line":278},[137,59987,59988],{"class":157},"    transcriptData.",[137,59990,8564],{"class":147},[137,59992,2774],{"class":157},[137,59994,59850],{"class":161},[137,59996,164],{"class":157},[137,59998,53918],{"class":161},[137,60000,219],{"class":157},[137,60002,222],{"class":143},[137,60004,256],{"class":157},[137,60006,60007,60009,60012,60014,60017,60019,60021,60024,60026,60029],{"class":139,"line":291},[137,60008,3008],{"class":143},[137,60010,60011],{"class":364}," el",[137,60013,151],{"class":143},[137,60015,60016],{"class":157}," transcriptEl.",[137,60018,3873],{"class":147},[137,60020,356],{"class":157},[137,60022,60023],{"class":284},"`[data-index=\"${",[137,60025,53918],{"class":157},[137,60027,60028],{"class":284},"}\"]`",[137,60030,1502],{"class":157},[137,60032,60033],{"class":139,"line":297},[137,60034,516],{"emptyLinePlaceholder":515},[137,60036,60037,60039,60042,60045,60048,60050,60053,60056],{"class":139,"line":302},[137,60038,5496],{"class":143},[137,60040,60041],{"class":157}," (currentTime ",[137,60043,60044],{"class":143},">=",[137,60046,60047],{"class":157}," seg.start ",[137,60049,3351],{"class":143},[137,60051,60052],{"class":157}," currentTime ",[137,60054,60055],{"class":143},"\u003C=",[137,60057,60058],{"class":157}," seg.end) {\n",[137,60060,60061,60064,60066,60068,60071],{"class":139,"line":662},[137,60062,60063],{"class":157},"            el.classList.",[137,60065,34393],{"class":147},[137,60067,356],{"class":157},[137,60069,60070],{"class":284},"\"active\"",[137,60072,1502],{"class":157},[137,60074,60075,60078,60081],{"class":139,"line":667},[137,60076,60077],{"class":157},"            el.",[137,60079,60080],{"class":147},"scrollIntoView",[137,60082,3175],{"class":157},[137,60084,60085,60088,60091],{"class":139,"line":786},[137,60086,60087],{"class":157},"                behavior: ",[137,60089,60090],{"class":284},"\"smooth\"",[137,60092,1961],{"class":157},[137,60094,60095,60098,60101],{"class":139,"line":798},[137,60096,60097],{"class":157},"                block: ",[137,60099,60100],{"class":284},"\"nearest\"",[137,60102,1961],{"class":157},[137,60104,60105,60108,60111],{"class":139,"line":803},[137,60106,60107],{"class":157},"                inline: ",[137,60109,60110],{"class":284},"\"center\"",[137,60112,1961],{"class":157},[137,60114,60115],{"class":139,"line":931},[137,60116,14336],{"class":157},[137,60118,60119,60121,60123],{"class":139,"line":1568},[137,60120,15729],{"class":157},[137,60122,24947],{"class":143},[137,60124,256],{"class":157},[137,60126,60127,60129,60132,60134,60136],{"class":139,"line":1573},[137,60128,60063],{"class":157},[137,60130,60131],{"class":147},"remove",[137,60133,356],{"class":157},[137,60135,60070],{"class":284},[137,60137,1502],{"class":157},[137,60139,60140],{"class":139,"line":1578},[137,60141,1966],{"class":157},[137,60143,60144],{"class":139,"line":1588},[137,60145,2832],{"class":157},[137,60147,60148],{"class":139,"line":1601},[137,60149,5422],{"class":157},[27,60151,60152],{},"Here's what's going on:",[1003,60154,60155,60163,60171,60174,60181],{},[1006,60156,4737,60157,60162],{},[42,60158,60159],{},[22,60160,60161],{},"timeupdate"," event fires continuously as the audio plays",[1006,60164,60165,60170],{},[42,60166,60167],{},[22,60168,60169],{},"currentTime"," tells us exactly where we are in the audio (in seconds)",[1006,60172,60173],{},"We loop through all segments and check if the current time falls within each segment's start and end times",[1006,60175,60176,60177,60180],{},"When there's a match, we add the ",[22,60178,60179],{},"active"," class to highlight that line",[1006,60182,60183,60188],{},[42,60184,60185],{},[22,60186,60187],{},"scrollIntoView()"," automatically scrolls the transcript to keep the active line visible - no manual scrolling needed!",[104,60190,60192],{"id":60191},"making-the-transcript-interactive","Making the Transcript Interactive",[27,60194,60195],{},"To let users jump to any part of the audio by clicking a transcript line, add this click handler:",[128,60197,60199],{"className":36884,"code":60198,"language":29196,"meta":133,"style":133}," \u002F\u002F Clickable transcript - jump to audio time\n    transcriptEl.addEventListener(\"click\", (e) => {\n      const seg = e.target.closest(\".segment\");\n      if (seg) {\n        const index = +seg.dataset.index;\n        audioEl.currentTime = transcriptData[index].start;\n        audioEl.play();\n      }\n    });\n  })\n  .catch(error => {\n    console.error('Error loading transcript:', error);\n  });\n\n",[22,60200,60201,60206,60227,60249,60257,60271,60281,60290,60294,60298,60303,60317,60330],{"__ignoreMap":133},[137,60202,60203],{"class":139,"line":140},[137,60204,60205],{"class":308}," \u002F\u002F Clickable transcript - jump to audio time\n",[137,60207,60208,60211,60213,60215,60217,60219,60221,60223,60225],{"class":139,"line":173},[137,60209,60210],{"class":157},"    transcriptEl.",[137,60212,4412],{"class":147},[137,60214,356],{"class":157},[137,60216,26258],{"class":284},[137,60218,24531],{"class":157},[137,60220,24534],{"class":161},[137,60222,219],{"class":157},[137,60224,222],{"class":143},[137,60226,256],{"class":157},[137,60228,60229,60231,60234,60236,60239,60242,60244,60247],{"class":139,"line":188},[137,60230,4429],{"class":143},[137,60232,60233],{"class":364}," seg",[137,60235,151],{"class":143},[137,60237,60238],{"class":157}," e.target.",[137,60240,60241],{"class":147},"closest",[137,60243,356],{"class":157},[137,60245,60246],{"class":284},"\".segment\"",[137,60248,1502],{"class":157},[137,60250,60251,60254],{"class":139,"line":269},[137,60252,60253],{"class":143},"      if",[137,60255,60256],{"class":157}," (seg) {\n",[137,60258,60259,60261,60264,60266,60268],{"class":139,"line":278},[137,60260,3008],{"class":143},[137,60262,60263],{"class":364}," index",[137,60265,151],{"class":143},[137,60267,361],{"class":143},[137,60269,60270],{"class":157},"seg.dataset.index;\n",[137,60272,60273,60276,60278],{"class":139,"line":291},[137,60274,60275],{"class":157},"        audioEl.currentTime ",[137,60277,253],{"class":143},[137,60279,60280],{"class":157}," transcriptData[index].start;\n",[137,60282,60283,60286,60288],{"class":139,"line":297},[137,60284,60285],{"class":157},"        audioEl.",[137,60287,12188],{"class":147},[137,60289,924],{"class":157},[137,60291,60292],{"class":139,"line":302},[137,60293,4847],{"class":157},[137,60295,60296],{"class":139,"line":662},[137,60297,2832],{"class":157},[137,60299,60300],{"class":139,"line":667},[137,60301,60302],{"class":157},"  })\n",[137,60304,60305,60307,60309,60311,60313,60315],{"class":139,"line":786},[137,60306,59749],{"class":157},[137,60308,2807],{"class":147},[137,60310,356],{"class":157},[137,60312,2812],{"class":161},[137,60314,59759],{"class":143},[137,60316,256],{"class":157},[137,60318,60319,60321,60323,60325,60328],{"class":139,"line":798},[137,60320,493],{"class":157},[137,60322,2812],{"class":147},[137,60324,356],{"class":157},[137,60326,60327],{"class":284},"'Error loading transcript:'",[137,60329,17836],{"class":157},[137,60331,60332],{"class":139,"line":803},[137,60333,60334],{"class":157},"  });\n",[27,60336,60337],{},"When someone clicks a transcript line:",[2569,60339,60340,60346,60352],{},[1006,60341,60342,60343],{},"We identify which segment was clicked using ",[22,60344,60345],{},"data-index",[1006,60347,60348,60349,60351],{},"We set the audio's ",[22,60350,60169],{}," to that segment's start time",[1006,60353,60354],{},"The audio automatically starts playing from that point",[104,60356,60358],{"id":60357},"adding-some-style","Adding Some Style",[27,60360,60361,60362,60365],{},"Now let's make it look good! Create a ",[22,60363,60364],{},"styles.css"," file. I'm keeping this simple so you can easily customise it to match your own design:",[128,60367,60369],{"className":23162,"code":60368,"language":23164,"meta":133,"style":133},"body {\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n    background: #efe8df;\n    padding: 2rem;\n}\n\n.container {\n    max-width: 1000px;\n    margin: 0 auto;\n    background: white;\n    border-radius: 16px;\n    padding: 3rem;\n    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);\n}\n\nh1 {\n    color: #173353;\n    text-align: center;\n    margin-bottom: 2rem;\n}\n\naudio {\n    width: 100%;\n    margin-bottom: 2rem;\n}\n\n.transcript {\n    max-height: 500px;\n    overflow-y: auto;\n    padding: 1.5rem;\n    background: white;\n    border-radius: 12px;\n}\n\n.segment {\n    padding: 0.75rem 1.25rem;\n    margin-bottom: 1rem;\n    border-radius: 8px;\n    cursor: pointer;\n    transition: all 0.3s ease;\n    border-left: 3px solid transparent;\n}\n\n.segment:hover {\n    background: rgba(191, 217, 219, 0.3);\n    border-left-color: #40979d;\n}\n\n.segment.active {\n    font-weight: 600;\n    background: rgba(191, 217, 219, 0.5);\n    border-left-color: #ee5f53;\n    transform: translateX(8px);\n}\n",[22,60370,60371,60377,60401,60413,60427,60431,60435,60442,60455,60469,60480,60493,60506,60547,60551,60555,60561,60572,60584,60597,60601,60605,60611,60625,60637,60641,60645,60652,60666,60678,60690,60700,60713,60717,60721,60728,60746,60758,60770,60782,60802,60821,60825,60829,60836,60866,60878,60882,60886,60893,60905,60932,60943,60961],{"__ignoreMap":133},[137,60372,60373,60375],{"class":139,"line":140},[137,60374,4065],{"class":4036},[137,60376,256],{"class":157},[137,60378,60379,60382,60384,60387,60390,60393,60396,60399],{"class":139,"line":173},[137,60380,60381],{"class":364},"    font-family",[137,60383,726],{"class":157},[137,60385,60386],{"class":364},"-apple-system",[137,60388,60389],{"class":157},", BlinkMacSystemFont, ",[137,60391,60392],{"class":284},"\"Segoe UI\"",[137,60394,60395],{"class":157},", Roboto, ",[137,60397,60398],{"class":364},"sans-serif",[137,60400,3276],{"class":157},[137,60402,60403,60406,60408,60411],{"class":139,"line":188},[137,60404,60405],{"class":364},"    background",[137,60407,726],{"class":157},[137,60409,60410],{"class":364},"#efe8df",[137,60412,3276],{"class":157},[137,60414,60415,60418,60420,60422,60425],{"class":139,"line":269},[137,60416,60417],{"class":364},"    padding",[137,60419,726],{"class":157},[137,60421,10345],{"class":364},[137,60423,60424],{"class":143},"rem",[137,60426,3276],{"class":157},[137,60428,60429],{"class":139,"line":278},[137,60430,510],{"class":157},[137,60432,60433],{"class":139,"line":291},[137,60434,516],{"emptyLinePlaceholder":515},[137,60436,60437,60440],{"class":139,"line":297},[137,60438,60439],{"class":147},".container",[137,60441,256],{"class":157},[137,60443,60444,60447,60449,60451,60453],{"class":139,"line":302},[137,60445,60446],{"class":364},"    max-width",[137,60448,726],{"class":157},[137,60450,37450],{"class":364},[137,60452,39722],{"class":143},[137,60454,3276],{"class":157},[137,60456,60457,60460,60462,60464,60467],{"class":139,"line":662},[137,60458,60459],{"class":364},"    margin",[137,60461,726],{"class":157},[137,60463,6044],{"class":364},[137,60465,60466],{"class":364}," auto",[137,60468,3276],{"class":157},[137,60470,60471,60473,60475,60478],{"class":139,"line":667},[137,60472,60405],{"class":364},[137,60474,726],{"class":157},[137,60476,60477],{"class":364},"white",[137,60479,3276],{"class":157},[137,60481,60482,60485,60487,60489,60491],{"class":139,"line":786},[137,60483,60484],{"class":364},"    border-radius",[137,60486,726],{"class":157},[137,60488,7697],{"class":364},[137,60490,39722],{"class":143},[137,60492,3276],{"class":157},[137,60494,60495,60497,60499,60502,60504],{"class":139,"line":798},[137,60496,60417],{"class":364},[137,60498,726],{"class":157},[137,60500,60501],{"class":364},"3",[137,60503,60424],{"class":143},[137,60505,3276],{"class":157},[137,60507,60508,60511,60513,60515,60518,60520,60523,60525,60528,60530,60532,60534,60536,60538,60540,60542,60545],{"class":139,"line":803},[137,60509,60510],{"class":364},"    box-shadow",[137,60512,726],{"class":157},[137,60514,6044],{"class":364},[137,60516,60517],{"class":364}," 4",[137,60519,39722],{"class":143},[137,60521,60522],{"class":364}," 20",[137,60524,39722],{"class":143},[137,60526,60527],{"class":364}," rgba",[137,60529,356],{"class":157},[137,60531,6044],{"class":364},[137,60533,164],{"class":157},[137,60535,6044],{"class":364},[137,60537,164],{"class":157},[137,60539,6044],{"class":364},[137,60541,164],{"class":157},[137,60543,60544],{"class":364},"0.1",[137,60546,1502],{"class":157},[137,60548,60549],{"class":139,"line":931},[137,60550,510],{"class":157},[137,60552,60553],{"class":139,"line":1568},[137,60554,516],{"emptyLinePlaceholder":515},[137,60556,60557,60559],{"class":139,"line":1573},[137,60558,17],{"class":4036},[137,60560,256],{"class":157},[137,60562,60563,60565,60567,60570],{"class":139,"line":1578},[137,60564,39703],{"class":364},[137,60566,726],{"class":157},[137,60568,60569],{"class":364},"#173353",[137,60571,3276],{"class":157},[137,60573,60574,60577,60579,60582],{"class":139,"line":1588},[137,60575,60576],{"class":364},"    text-align",[137,60578,726],{"class":157},[137,60580,60581],{"class":364},"center",[137,60583,3276],{"class":157},[137,60585,60586,60589,60591,60593,60595],{"class":139,"line":1601},[137,60587,60588],{"class":364},"    margin-bottom",[137,60590,726],{"class":157},[137,60592,10345],{"class":364},[137,60594,60424],{"class":143},[137,60596,3276],{"class":157},[137,60598,60599],{"class":139,"line":3802},[137,60600,510],{"class":157},[137,60602,60603],{"class":139,"line":3808},[137,60604,516],{"emptyLinePlaceholder":515},[137,60606,60607,60609],{"class":139,"line":3822},[137,60608,59550],{"class":4036},[137,60610,256],{"class":157},[137,60612,60613,60616,60618,60620,60623],{"class":139,"line":3827},[137,60614,60615],{"class":364},"    width",[137,60617,726],{"class":157},[137,60619,2077],{"class":364},[137,60621,60622],{"class":143},"%",[137,60624,3276],{"class":157},[137,60626,60627,60629,60631,60633,60635],{"class":139,"line":3832},[137,60628,60588],{"class":364},[137,60630,726],{"class":157},[137,60632,10345],{"class":364},[137,60634,60424],{"class":143},[137,60636,3276],{"class":157},[137,60638,60639],{"class":139,"line":3840},[137,60640,510],{"class":157},[137,60642,60643],{"class":139,"line":3846},[137,60644,516],{"emptyLinePlaceholder":515},[137,60646,60647,60650],{"class":139,"line":3861},[137,60648,60649],{"class":147},".transcript",[137,60651,256],{"class":157},[137,60653,60654,60657,60659,60662,60664],{"class":139,"line":3883},[137,60655,60656],{"class":364},"    max-height",[137,60658,726],{"class":157},[137,60660,60661],{"class":364},"500",[137,60663,39722],{"class":143},[137,60665,3276],{"class":157},[137,60667,60668,60671,60673,60676],{"class":139,"line":3896},[137,60669,60670],{"class":364},"    overflow-y",[137,60672,726],{"class":157},[137,60674,60675],{"class":364},"auto",[137,60677,3276],{"class":157},[137,60679,60680,60682,60684,60686,60688],{"class":139,"line":3901},[137,60681,60417],{"class":364},[137,60683,726],{"class":157},[137,60685,12220],{"class":364},[137,60687,60424],{"class":143},[137,60689,3276],{"class":157},[137,60691,60692,60694,60696,60698],{"class":139,"line":3906},[137,60693,60405],{"class":364},[137,60695,726],{"class":157},[137,60697,60477],{"class":364},[137,60699,3276],{"class":157},[137,60701,60702,60704,60706,60709,60711],{"class":139,"line":3911},[137,60703,60484],{"class":364},[137,60705,726],{"class":157},[137,60707,60708],{"class":364},"12",[137,60710,39722],{"class":143},[137,60712,3276],{"class":157},[137,60714,60715],{"class":139,"line":4666},[137,60716,510],{"class":157},[137,60718,60719],{"class":139,"line":4672},[137,60720,516],{"emptyLinePlaceholder":515},[137,60722,60723,60726],{"class":139,"line":4680},[137,60724,60725],{"class":147},".segment",[137,60727,256],{"class":157},[137,60729,60730,60732,60734,60737,60739,60742,60744],{"class":139,"line":4711},[137,60731,60417],{"class":364},[137,60733,726],{"class":157},[137,60735,60736],{"class":364},"0.75",[137,60738,60424],{"class":143},[137,60740,60741],{"class":364}," 1.25",[137,60743,60424],{"class":143},[137,60745,3276],{"class":157},[137,60747,60748,60750,60752,60754,60756],{"class":139,"line":4716},[137,60749,60588],{"class":364},[137,60751,726],{"class":157},[137,60753,6065],{"class":364},[137,60755,60424],{"class":143},[137,60757,3276],{"class":157},[137,60759,60760,60762,60764,60766,60768],{"class":139,"line":4721},[137,60761,60484],{"class":364},[137,60763,726],{"class":157},[137,60765,14713],{"class":364},[137,60767,39722],{"class":143},[137,60769,3276],{"class":157},[137,60771,60772,60775,60777,60780],{"class":139,"line":4727},[137,60773,60774],{"class":364},"    cursor",[137,60776,726],{"class":157},[137,60778,60779],{"class":364},"pointer",[137,60781,3276],{"class":157},[137,60783,60784,60787,60789,60791,60794,60797,60800],{"class":139,"line":4732},[137,60785,60786],{"class":364},"    transition",[137,60788,726],{"class":157},[137,60790,49391],{"class":364},[137,60792,60793],{"class":364}," 0.3",[137,60795,60796],{"class":143},"s",[137,60798,60799],{"class":364}," ease",[137,60801,3276],{"class":157},[137,60803,60804,60807,60809,60811,60813,60816,60819],{"class":139,"line":5006},[137,60805,60806],{"class":364},"    border-left",[137,60808,726],{"class":157},[137,60810,60501],{"class":364},[137,60812,39722],{"class":143},[137,60814,60815],{"class":364}," solid",[137,60817,60818],{"class":364}," transparent",[137,60820,3276],{"class":157},[137,60822,60823],{"class":139,"line":5014},[137,60824,510],{"class":157},[137,60826,60827],{"class":139,"line":14343},[137,60828,516],{"emptyLinePlaceholder":515},[137,60830,60831,60834],{"class":139,"line":24199},[137,60832,60833],{"class":147},".segment:hover",[137,60835,256],{"class":157},[137,60837,60838,60840,60842,60845,60847,60850,60852,60855,60857,60860,60862,60864],{"class":139,"line":24773},[137,60839,60405],{"class":364},[137,60841,726],{"class":157},[137,60843,60844],{"class":364},"rgba",[137,60846,356],{"class":157},[137,60848,60849],{"class":364},"191",[137,60851,164],{"class":157},[137,60853,60854],{"class":364},"217",[137,60856,164],{"class":157},[137,60858,60859],{"class":364},"219",[137,60861,164],{"class":157},[137,60863,47592],{"class":364},[137,60865,1502],{"class":157},[137,60867,60868,60871,60873,60876],{"class":139,"line":24778},[137,60869,60870],{"class":364},"    border-left-color",[137,60872,726],{"class":157},[137,60874,60875],{"class":364},"#40979d",[137,60877,3276],{"class":157},[137,60879,60880],{"class":139,"line":24783},[137,60881,510],{"class":157},[137,60883,60884],{"class":139,"line":24793},[137,60885,516],{"emptyLinePlaceholder":515},[137,60887,60888,60891],{"class":139,"line":24827},[137,60889,60890],{"class":147},".segment.active",[137,60892,256],{"class":157},[137,60894,60895,60898,60900,60903],{"class":139,"line":24857},[137,60896,60897],{"class":364},"    font-weight",[137,60899,726],{"class":157},[137,60901,60902],{"class":364},"600",[137,60904,3276],{"class":157},[137,60906,60907,60909,60911,60913,60915,60917,60919,60921,60923,60925,60927,60930],{"class":139,"line":24862},[137,60908,60405],{"class":364},[137,60910,726],{"class":157},[137,60912,60844],{"class":364},[137,60914,356],{"class":157},[137,60916,60849],{"class":364},[137,60918,164],{"class":157},[137,60920,60854],{"class":364},[137,60922,164],{"class":157},[137,60924,60859],{"class":364},[137,60926,164],{"class":157},[137,60928,60929],{"class":364},"0.5",[137,60931,1502],{"class":157},[137,60933,60934,60936,60938,60941],{"class":139,"line":24867},[137,60935,60870],{"class":364},[137,60937,726],{"class":157},[137,60939,60940],{"class":364},"#ee5f53",[137,60942,3276],{"class":157},[137,60944,60945,60948,60950,60953,60955,60957,60959],{"class":139,"line":24884},[137,60946,60947],{"class":364},"    transform",[137,60949,726],{"class":157},[137,60951,60952],{"class":364},"translateX",[137,60954,356],{"class":157},[137,60956,14713],{"class":364},[137,60958,39722],{"class":143},[137,60960,1502],{"class":157},[137,60962,60963],{"class":139,"line":24892},[137,60964,510],{"class":157},[27,60966,60967],{},"Here are the key styling details:",[1003,60969,60970,60975,60980,60983],{},[1006,60971,60972,60974],{},[22,60973,60725],{}," styles each line of transcript",[1006,60976,60977,60979],{},[22,60978,60890],{}," highlights the currently playing line",[1006,60981,60982],{},"The left border and background color change to make it really obvious what's playing",[1006,60984,60985,60988],{},[22,60986,60987],{},"transform: translateX(8px)"," gives the active text a subtle slide to the right for extra emphasis",[104,60990,60992],{"id":60991},"testing-your-player","Testing Your Player",[27,60994,60995],{},"Time to see it in action! Here's what to do:",[2569,60997,60998,61026,61032],{},[1006,60999,61000,61001],{},"Put all your files in the same folder:\n",[1003,61002,61003,61007,61011,61015,61020],{},[1006,61004,61005],{},[22,61006,23204],{},[1006,61008,61009],{},[22,61010,59721],{},[1006,61012,61013],{},[22,61014,60364],{},[1006,61016,61017,61019],{},[22,61018,57240],{}," (your audio file)",[1006,61021,61022,61025],{},[22,61023,61024],{},"podcast_episode_transcript.json"," (your transcript)",[1006,61027,61028,61029,61031],{},"Open ",[22,61030,23204],{}," in your web browser",[1006,61033,61028,61034,61036],{},[22,61035,23204],{}," in your web browser and press play to see the transcript highlight in sync with the audio",[104,61038,59075],{"id":59074},[27,61040,61041],{},"And there you have it - a fully functional audio player with synchronised transcript highlighting! I love that it's all built with vanilla JavaScript. No frameworks, no dependencies, just clean, simple code.",[27,61043,61044,61045,61049],{},"You can find the complete code for this project on ",[45,61046,50876],{"href":61047,"target":2716,"rel":61048},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Faudio-synced-transcription-player",[2718,2719],". I hope you found this tutorial helpful.",[2617,61051,61052],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":133,"searchDepth":173,"depth":173,"links":61054},[61055,61056,61057,61058,61059,61060,61061,61062,61063,61064],{"id":59180,"depth":173,"text":59181},{"id":57168,"depth":173,"text":57169},{"id":59217,"depth":173,"text":59218},{"id":59383,"depth":173,"text":59384},{"id":59714,"depth":173,"text":59715},{"id":59932,"depth":173,"text":59933},{"id":60191,"depth":173,"text":60192},{"id":60357,"depth":173,"text":60358},{"id":60991,"depth":173,"text":60992},{"id":59074,"depth":173,"text":59075},"Learn how to build a fully interactive audio player with real-time transcript highlighting using simple HTML, CSS, and JavaScript. This tutorial walks you through syncing audio with precise caption timestamps, automatically highlighting each spoken line, smooth auto-scrolling, and letting listeners jump to any moment by clicking the transcript. Perfect for podcasts, tutorials, and spoken-word content.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1763271005\u002Fblog\u002Fmake-your-audio-play-with-real-time-transcript-highlighting\u002Fmake-your-audio-play-with-real-time-transcript-highlighting_hgmjsl",[61068,61069,61070,61071,61072,61073,61074,61075,61076,61077,61078,61079],"audio transcript highlighting","real-time transcript sync","audio player with transcript","HTML audio transcript tutorial","JavaScript audio transcript sync","synced captions for audio","interactive transcript player","podcast transcript highlighting","audio to transcript JSON timestamps","OpenAI Whisper transcript tutorial","build audio player HTML CSS JS","synced audio playback JavaScript",{},"\u002F2025\u002F11\u002F16\u002Fmake-your-audio-play-with-real-time-transcript-highlighting","16th November 2025",{"title":59137,"description":61065},"2025\u002F11\u002F16\u002Fmake-your-audio-play-with-real-time-transcript-highlighting","Qf1VxKZTDveTLxKCRO5fcEdDXX4QEjgAc1gnSEb6V_4",{"id":61087,"title":61088,"articleTags":61089,"author":11,"blog":12,"body":61090,"description":66535,"extension":2649,"image":66536,"keywords":66537,"meta":66553,"navigation":515,"path":66554,"published":66555,"readTime":798,"seo":66556,"stem":66557,"type":2662,"__hash__":66558},"content\u002F2025\u002F12\u002F20\u002Fmaking-a-face-follow-your-cursor-with-ai-generated-images.md","Making a Face Follow Your Cursor with AI‑Generated Images",[27886,10,2669],{"type":14,"value":61091,"toc":66518},[61092,61095,61109,61111,61115,61118,61127,61145,61152,61170,61173,61176,61180,61183,61220,61227,61231,61252,61255,61266,61269,61300,61303,61328,61331,61335,61338,61345,61348,61383,61393,61411,61423,61435,61459,61469,61473,61476,61480,61486,61577,61580,61589,61608,61611,61615,61620,61699,61702,61706,61709,61880,61896,61900,61907,62354,62372,62375,62389,62396,62400,62403,62601,62615,62618,62621,62634,62637,62643,62646,62650,62653,62675,62681,64788,64791,64795,64800,65239,65248,65255,65269,65272,65348,65351,65355,65365,65385,65388,65862,65865,65875,65884,65902,65912,65916,65919,65922,65937,65940,65976,65979,66454,66457,66503,66506,66508,66515],[17,61093,61088],{"id":61094},"making-a-face-follow-your-cursor-with-aigenerated-images",[27,61096,61097],{},[30,61098,61099,36,61101,40,61103],{},[33,61100],{"value":35},[33,61102],{"value":39},[42,61104,61105],{},[45,61106,61107],{"href":47},[33,61108],{"value":50},[52,61110],{":tags":54},[56,61112],{":audio-src":61113,":transcript-src":61114},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F12\u002F20\u002Fmaking-a-face-follow-your-cursor-with-ai-generated-images\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F12\u002F20\u002Fmaking-a-face-follow-your-cursor-with-ai-generated-images\u002Fsummary.json",[61116,61117],"portrait-eye-tracker",{},[27,61119,61120,61121,61126],{},"This idea isn't my original invention. I discovered it while scrolling through X and saw ",[45,61122,61125],{"href":61123,"target":2716,"rel":61124},"https:\u002F\u002Fx.com\u002Fwesbos",[2718,2719],"Wes Bos"," share a cool video of a face that follows your cursor around the screen. It caught my attention right away.",[1003,61128,61129,61137],{},[1006,61130,61131,61132],{},"Here's the video Wes posted: ",[45,61133,61136],{"href":61134,"target":2716,"rel":61135},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=sPdRCYbO6so",[2718,2719],"Watch on YouTube",[1006,61138,61139,61140],{},"And here's his GitHub repository if you want to dive into his code: ",[45,61141,61144],{"href":61142,"target":2716,"rel":61143},"https:\u002F\u002Fgithub.com\u002Fwesbos\u002Feye-ballz",[2718,2719],"wesbos\u002Feye-ballz",[27,61146,61147,61148,61151],{},"In his post, Wes mentioned that he got the inspiration from ",[42,61149,61150],{},"Kylan O'Connor",", who was the first person to implement this interactive face-tracking effect on his personal website.",[1003,61153,61154,61162],{},[1006,61155,61156,61157],{},"Check out Kylan's website here: ",[45,61158,61161],{"href":61159,"target":2716,"rel":61160},"https:\u002F\u002Fwww.kylanoconnor.com\u002F",[2718,2719],"kylanoconnor.com",[1006,61163,61164,61165],{},"Follow him on Twitter\u002FX: ",[45,61166,61169],{"href":61167,"target":2716,"rel":61168},"https:\u002F\u002Fx.com\u002Fkylancodes",[2718,2719],"@kylancodes",[27,61171,61172],{},"So what's my contribution? I put my own spin on it. I didn't follow the provided code examples - I prefer building things from scratch. I ended up with a very similar result.",[27,61174,61175],{},"In this post, I'll walk you through exactly what I did and why I did it this way.",[104,61177,61179],{"id":61178},"the-idea","The Idea",[27,61181,61182],{},"Let me break down what we're trying to accomplish. The goal is to create an interactive experience where a face (in this case, my face) tracks and follows the user's cursor as they move their mouse around the screen. Here's how it works:",[2569,61184,61185,61192,61203,61206],{},[1006,61186,61187,61188,61191],{},"We start with ",[42,61189,61190],{},"one photo"," of a face - ideally a clear, well-lit shot where the person is looking straight at the camera.",[1006,61193,61194,61195],{},"We use an AI model to generate many variations of that face, each showing:\n",[1003,61196,61197,61200],{},[1006,61198,61199],{},"The head tilted up or down at different angles.",[1006,61201,61202],{},"The eyes looking left, right, up, or down in various degrees.",[1006,61204,61205],{},"We download all those generated images locally to our machine.",[1006,61207,61208,61209],{},"In the web browser, we write code that:\n",[1003,61210,61211,61214,61217],{},[1006,61212,61213],{},"Preloads all the images so they're ready to display instantly.",[1006,61215,61216],{},"Shows only one image at a time in the center of the screen.",[1006,61218,61219],{},"When the user moves their mouse (or tilts their device on a phone), the code calculates which direction they're pointing toward, picks the image that best matches that direction, and instantly displays it.",[27,61221,61222,61223,61226],{},"This approach uses ",[42,61224,61225],{},"no live AI processing in the browser"," - we're not running machine learning models on the fly. We're simply swapping between pre-generated images quickly, making the experience smooth and responsive even on slower devices.",[104,61228,61230],{"id":61229},"the-ai-model","The AI Model",[27,61232,61233,61234,61241,61242,61247,61248,1017],{},"To generate all these face variations, I used an open-source AI model called ",[45,61235,61238],{"href":61236,"target":2716,"rel":61237},"https:\u002F\u002Freplicate.com\u002Ffofr\u002Fexpression-editor",[2718,2719],[42,61239,61240],{},"fofr\u002Fexpression-editor",". You can download and run it locally on your machine using Docker - though you'll need an NVIDIA GPU. If you want to go this route, I made a ",[45,61243,61246],{"href":61244,"target":2716,"rel":61245},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Ffofr-expression-editor-docker",[2718,2719],"GitHub repo"," to help you get started with Docker more easily. Alternatively, you can use it hosted on a platform called Replicate, which is what I did for this article. It costs only $0.0015 per image generation. You can find the model's page on X ",[45,61249,10647],{"href":61250,"target":2716,"rel":61251},"https:\u002F\u002Fx.com\u002FfofrAI",[2718,2719],[27,61253,61254],{},"This model is specifically designed for editing facial expressions and orientations. Here's how it works - you give it:",[1003,61256,61257,61260,61263],{},[1006,61258,61259],{},"a base image URL (a link to a clear photo of your face),",[1006,61261,61262],{},"some numeric parameters that tell it how to modify the face,",[1006,61264,61265],{},"and it processes everything and returns a new image with those adjustments applied.",[27,61267,61268],{},"The model has many parameters you can tweak - you can make the person smile, wink, open their mouth, raise their eyebrows, and more. But for this project, I kept things simple and used only three specific parameters:",[1003,61270,61271,61281,61291],{},[1006,61272,61273,61276,61277,61280],{},[22,61274,61275],{},"rotate_pitch"," – Controls how much the head tilts up or down. I used a range from ",[42,61278,61279],{},"-20 to 20",", where negative values tilt the head down and positive values tilt it up.",[1006,61282,61283,61286,61287,61290],{},[22,61284,61285],{},"pupil_x"," – Moves the eyes horizontally, making them look left or right. I used a range from ",[42,61288,61289],{},"-15 to 15",", where negative values look left and positive values look right.",[1006,61292,61293,61296,61297,61299],{},[22,61294,61295],{},"pupil_y"," – Moves the eyes vertically, making them look up or down. I used a range from ",[42,61298,61289],{},", where negative values look down and positive values look up.",[27,61301,61302],{},"We're not going to generate images for every possible combination of values - that would create thousands of images! Instead, we'll generate a grid of strategic combinations by sampling values at regular intervals. Here's what I chose:",[1003,61304,61305,61314,61322],{},[1006,61306,61307,61308,61310,61311],{},"For ",[22,61309,61275],{},", I used five values: ",[22,61312,61313],{},"[-20, -10, 0, 10, 20]",[1006,61315,61307,61316,61318,61319],{},[22,61317,61285],{},", I used three values: ",[22,61320,61321],{},"[-15, 0, 15]",[1006,61323,61307,61324,61318,61326],{},[22,61325,61295],{},[22,61327,61321],{},[27,61329,61330],{},"If you do the math, that's 5 pitch angles × 3 horizontal eye positions × 3 vertical eye positions = 45 total images. That's a manageable number that gives us enough variety to create a smooth tracking effect.",[104,61332,61334],{"id":61333},"setting-up-a-small-nodejs-project","Setting Up a Small Node.js Project",[27,61336,61337],{},"We're going to use Node.js to create a script that generates images with the AI model using the official Replicate package. Here's what you'll need to have installed:",[1003,61339,61340,61342],{},[1006,61341,2669],{},[1006,61343,61344],{},"A Replicate account (it's free to sign up, but you'll need to set up payment details) and an API token, which you can get after creating your account and setting up payment.",[27,61346,61347],{},"Once you have those prerequisites ready, let's set up the project. Open your terminal, navigate to an empty folder where you want to work, and run these commands to initialize the project:",[128,61349,61351],{"className":8665,"code":61350,"language":8667,"meta":133,"style":133},"npm init -y\nnpm install replicate\nnpm install -D tsx @types\u002Fnode\n",[22,61352,61353,61361,61370],{"__ignoreMap":133},[137,61354,61355,61357,61359],{"class":139,"line":140},[137,61356,9536],{"class":147},[137,61358,9539],{"class":284},[137,61360,57291],{"class":364},[137,61362,61363,61365,61367],{"class":139,"line":173},[137,61364,9536],{"class":147},[137,61366,10268],{"class":284},[137,61368,61369],{"class":284}," replicate\n",[137,61371,61372,61374,61376,61378,61380],{"class":139,"line":188},[137,61373,9536],{"class":147},[137,61375,10268],{"class":284},[137,61377,23060],{"class":364},[137,61379,49162],{"class":284},[137,61381,61382],{"class":284}," @types\u002Fnode\n",[27,61384,61385,61386,61388,61389,61392],{},"Next, we need to store our Replicate API token securely. We'll create a file called ",[22,61387,13489],{}," that stores our secret token. Run this command in your terminal (but replace ",[22,61390,61391],{},"your_token_here"," with your actual API token from Replicate):",[128,61394,61396],{"className":8665,"code":61395,"language":8667,"meta":133,"style":133},"echo \"REPLICATE_API_TOKEN=your_token_here\" > .env\n",[22,61397,61398],{"__ignoreMap":133},[137,61399,61400,61403,61406,61408],{"class":139,"line":140},[137,61401,61402],{"class":364},"echo",[137,61404,61405],{"class":284}," \"REPLICATE_API_TOKEN=your_token_here\"",[137,61407,48819],{"class":143},[137,61409,61410],{"class":284}," .env\n",[3244,61412,61413],{},[27,61414,61415,61416,61418,61419,61422],{},"Add the ",[22,61417,13489],{}," file to your ",[22,61420,61421],{},".gitignore"," file. This prevents your API token from being exposed if you push the repository to GitHub or share it publicly.",[27,61424,61425,61426,61428,61429,61431,61432,61434],{},"Now, let's add a convenient script to our ",[22,61427,5140],{}," file that will let us easily run our TypeScript code. Open ",[22,61430,5140],{}," in your text editor and add this inside the ",[22,61433,5164],{}," section:",[128,61436,61438],{"className":5155,"code":61437,"language":5157,"meta":133,"style":133},"\"scripts\": {\n  \"start\": \"tsx --env-file=.env .\u002Findex.ts\"\n}\n",[22,61439,61440,61446,61455],{"__ignoreMap":133},[137,61441,61442,61444],{"class":139,"line":140},[137,61443,5164],{"class":284},[137,61445,1819],{"class":157},[137,61447,61448,61451,61453],{"class":139,"line":173},[137,61449,61450],{"class":364},"  \"start\"",[137,61452,726],{"class":157},[137,61454,57362],{"class":284},[137,61456,61457],{"class":139,"line":188},[137,61458,510],{"class":157},[27,61460,61461,61462,61465,61466,61468],{},"This script runs our ",[22,61463,61464],{},"index.ts"," file and loads the ",[22,61467,13489],{}," file without needing extra packages to handle that.",[104,61470,61472],{"id":61471},"generating-all-the-face-images-with-nodejs-and-replicate","Generating All the Face Images with Node.js and Replicate",[27,61474,61475],{},"Now we get to the really exciting part - writing the code that will automatically generate all 45 variations of our face! Let's break this down into manageable pieces.",[123,61477,61479],{"id":61478},"defining-the-parameter-grid","Defining the Parameter Grid",[27,61481,61482,61483,61485],{},"First, let's create a new file called ",[22,61484,61464],{}," in our project folder. At the top of this file, we'll define the sets of values we want to sample for each parameter. Remember, we're choosing strategic values rather than trying every possible number:",[128,61487,61489],{"className":13299,"code":61488,"language":13301,"meta":133,"style":133},"const pitchValues = [-20, -10, 0, 10, 20];\nconst pupilXValues = [-15, 0, 15];\nconst pupilYValues = [-15, 0, 15];\n",[22,61490,61491,61526,61552],{"__ignoreMap":133},[137,61492,61493,61495,61498,61500,61502,61504,61506,61508,61510,61512,61514,61516,61518,61520,61522,61524],{"class":139,"line":140},[137,61494,3077],{"class":143},[137,61496,61497],{"class":364}," pitchValues",[137,61499,151],{"class":143},[137,61501,22130],{"class":157},[137,61503,8215],{"class":143},[137,61505,14727],{"class":364},[137,61507,164],{"class":157},[137,61509,8215],{"class":143},[137,61511,33489],{"class":364},[137,61513,164],{"class":157},[137,61515,6044],{"class":364},[137,61517,164],{"class":157},[137,61519,33489],{"class":364},[137,61521,164],{"class":157},[137,61523,14727],{"class":364},[137,61525,5727],{"class":157},[137,61527,61528,61530,61533,61535,61537,61539,61542,61544,61546,61548,61550],{"class":139,"line":173},[137,61529,3077],{"class":143},[137,61531,61532],{"class":364}," pupilXValues",[137,61534,151],{"class":143},[137,61536,22130],{"class":157},[137,61538,8215],{"class":143},[137,61540,61541],{"class":364},"15",[137,61543,164],{"class":157},[137,61545,6044],{"class":364},[137,61547,164],{"class":157},[137,61549,61541],{"class":364},[137,61551,5727],{"class":157},[137,61553,61554,61556,61559,61561,61563,61565,61567,61569,61571,61573,61575],{"class":139,"line":188},[137,61555,3077],{"class":143},[137,61557,61558],{"class":364}," pupilYValues",[137,61560,151],{"class":143},[137,61562,22130],{"class":157},[137,61564,8215],{"class":143},[137,61566,61541],{"class":364},[137,61568,164],{"class":157},[137,61570,6044],{"class":364},[137,61572,164],{"class":157},[137,61574,61541],{"class":364},[137,61576,5727],{"class":157},[27,61578,61579],{},"These arrays represent all the different angles and eye positions we want to generate. We'll loop through every combination of these values later.",[27,61581,61582,61583,61588],{},"We also need to specify where our base face image is located. Upload your face photo to an image hosting service - I used ",[45,61584,61587],{"href":61585,"target":2716,"rel":61586},"https:\u002F\u002Fcloudinary.com\u002F",[2718,2719],"Cloudinary",", but any service that gives you a direct URL will work. Then add this line to your code:",[128,61590,61592],{"className":13299,"code":61591,"language":13301,"meta":133,"style":133},"const IMAGE_URL = \"https:\u002F\u002Fyour-image-host.com\u002Fpath\u002Fto\u002Fyour-face.jpg\";\n",[22,61593,61594],{"__ignoreMap":133},[137,61595,61596,61598,61601,61603,61606],{"class":139,"line":140},[137,61597,3077],{"class":143},[137,61599,61600],{"class":364}," IMAGE_URL",[137,61602,151],{"class":143},[137,61604,61605],{"class":284}," \"https:\u002F\u002Fyour-image-host.com\u002Fpath\u002Fto\u002Fyour-face.jpg\"",[137,61607,3276],{"class":157},[27,61609,61610],{},"Replace that URL with your actual uploaded face photo. Use a clear, well-lit portrait where you're looking straight at the camera - this gives the AI model the best starting point for generating variations.",[123,61612,61614],{"id":61613},"connecting-to-replicate","Connecting to Replicate",[27,61616,61617,61618,9772],{},"Now let's import the libraries we need and set up our connection to Replicate. Add these lines at the top of your ",[22,61619,61464],{},[128,61621,61623],{"className":13299,"code":61622,"language":13301,"meta":133,"style":133},"import Replicate from \"replicate\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nconst replicate = new Replicate({\n    auth: process.env.REPLICATE_API_TOKEN,\n});\n",[22,61624,61625,61639,61652,61665,61669,61685,61695],{"__ignoreMap":133},[137,61626,61627,61629,61632,61634,61637],{"class":139,"line":140},[137,61628,10287],{"class":143},[137,61630,61631],{"class":157}," Replicate ",[137,61633,10954],{"class":143},[137,61635,61636],{"class":284}," \"replicate\"",[137,61638,3276],{"class":157},[137,61640,61641,61643,61645,61647,61650],{"class":139,"line":173},[137,61642,10287],{"class":143},[137,61644,48449],{"class":157},[137,61646,10954],{"class":143},[137,61648,61649],{"class":284}," \"node:fs\"",[137,61651,3276],{"class":157},[137,61653,61654,61656,61658,61660,61663],{"class":139,"line":188},[137,61655,10287],{"class":143},[137,61657,48463],{"class":157},[137,61659,10954],{"class":143},[137,61661,61662],{"class":284}," \"node:path\"",[137,61664,3276],{"class":157},[137,61666,61667],{"class":139,"line":269},[137,61668,516],{"emptyLinePlaceholder":515},[137,61670,61671,61673,61676,61678,61680,61683],{"class":139,"line":278},[137,61672,3077],{"class":143},[137,61674,61675],{"class":364}," replicate",[137,61677,151],{"class":143},[137,61679,1426],{"class":143},[137,61681,61682],{"class":147}," Replicate",[137,61684,3175],{"class":157},[137,61686,61687,61690,61693],{"class":139,"line":291},[137,61688,61689],{"class":157},"    auth: process.env.",[137,61691,61692],{"class":364},"REPLICATE_API_TOKEN",[137,61694,1961],{"class":157},[137,61696,61697],{"class":139,"line":297},[137,61698,5422],{"class":157},[27,61700,61701],{},"We import the Replicate library to communicate with the AI model, then create a new Replicate client and authenticate it using our API token.",[123,61703,61705],{"id":61704},"looping-over-all-combinations","Looping Over All Combinations",[27,61707,61708],{},"Next, we need to generate all possible combinations of our three parameters. Let's create a TypeScript interface to define what each combination looks like, and then a function that builds all the combinations:",[128,61710,61712],{"className":13299,"code":61711,"language":13301,"meta":133,"style":133},"interface ImageParams {\n    rotate_pitch: number;\n    pupil_x: number;\n    pupil_y: number;\n}\n\nfunction getCombinations(): ImageParams[] {\n    const combos: ImageParams[] = [];\n    for (const pitch of pitchValues) {\n        for (const px of pupilXValues) {\n            for (const py of pupilYValues) {\n                combos.push({ rotate_pitch: pitch, pupil_x: px, pupil_y: py });\n            }\n        }\n    }\n    return combos;\n}\n",[22,61713,61714,61723,61734,61745,61756,61760,61764,61781,61798,61814,61830,61847,61857,61861,61865,61869,61876],{"__ignoreMap":133},[137,61715,61716,61718,61721],{"class":139,"line":140},[137,61717,48479],{"class":143},[137,61719,61720],{"class":147}," ImageParams",[137,61722,256],{"class":157},[137,61724,61725,61728,61730,61732],{"class":139,"line":173},[137,61726,61727],{"class":161},"    rotate_pitch",[137,61729,894],{"class":143},[137,61731,31395],{"class":364},[137,61733,3276],{"class":157},[137,61735,61736,61739,61741,61743],{"class":139,"line":188},[137,61737,61738],{"class":161},"    pupil_x",[137,61740,894],{"class":143},[137,61742,31395],{"class":364},[137,61744,3276],{"class":157},[137,61746,61747,61750,61752,61754],{"class":139,"line":269},[137,61748,61749],{"class":161},"    pupil_y",[137,61751,894],{"class":143},[137,61753,31395],{"class":364},[137,61755,3276],{"class":157},[137,61757,61758],{"class":139,"line":278},[137,61759,510],{"class":157},[137,61761,61762],{"class":139,"line":291},[137,61763,516],{"emptyLinePlaceholder":515},[137,61765,61766,61768,61771,61774,61776,61778],{"class":139,"line":297},[137,61767,483],{"class":143},[137,61769,61770],{"class":147}," getCombinations",[137,61772,61773],{"class":157},"()",[137,61775,894],{"class":143},[137,61777,61720],{"class":147},[137,61779,61780],{"class":157},"[] {\n",[137,61782,61783,61785,61788,61790,61792,61794,61796],{"class":139,"line":302},[137,61784,4177],{"class":143},[137,61786,61787],{"class":364}," combos",[137,61789,894],{"class":143},[137,61791,61720],{"class":147},[137,61793,58220],{"class":157},[137,61795,253],{"class":143},[137,61797,8556],{"class":157},[137,61799,61800,61802,61804,61806,61809,61811],{"class":139,"line":662},[137,61801,21473],{"class":143},[137,61803,158],{"class":157},[137,61805,3077],{"class":143},[137,61807,61808],{"class":364}," pitch",[137,61810,48913],{"class":143},[137,61812,61813],{"class":157}," pitchValues) {\n",[137,61815,61816,61818,61820,61822,61825,61827],{"class":139,"line":667},[137,61817,58368],{"class":143},[137,61819,158],{"class":157},[137,61821,3077],{"class":143},[137,61823,61824],{"class":364}," px",[137,61826,48913],{"class":143},[137,61828,61829],{"class":157}," pupilXValues) {\n",[137,61831,61832,61835,61837,61839,61842,61844],{"class":139,"line":786},[137,61833,61834],{"class":143},"            for",[137,61836,158],{"class":157},[137,61838,3077],{"class":143},[137,61840,61841],{"class":364}," py",[137,61843,48913],{"class":143},[137,61845,61846],{"class":157}," pupilYValues) {\n",[137,61848,61849,61852,61854],{"class":139,"line":798},[137,61850,61851],{"class":157},"                combos.",[137,61853,8583],{"class":147},[137,61855,61856],{"class":157},"({ rotate_pitch: pitch, pupil_x: px, pupil_y: py });\n",[137,61858,61859],{"class":139,"line":803},[137,61860,760],{"class":157},[137,61862,61863],{"class":139,"line":931},[137,61864,1966],{"class":157},[137,61866,61867],{"class":139,"line":1568},[137,61868,294],{"class":157},[137,61870,61871,61873],{"class":139,"line":1573},[137,61872,176],{"class":143},[137,61874,61875],{"class":157}," combos;\n",[137,61877,61878],{"class":139,"line":1578},[137,61879,510],{"class":157},[27,61881,61882,61883,14528,61885,61887,61888,61891,61892,61895],{},"This function uses three nested loops to iterate through every combination of pitch, ",[22,61884,61285],{},[22,61886,61295],{}," values. For example, it will create combinations like ",[22,61889,61890],{},"{pitch: -20, pupil_x: -15, pupil_y: -15}",", then ",[22,61893,61894],{},"{pitch: -20, pupil_x: -15, pupil_y: 0}",", and so on, until we have all 45 combinations stored in an array.",[123,61897,61899],{"id":61898},"calling-the-ai-model-and-saving-each-generated-image","Calling the AI Model and Saving Each Generated Image",[27,61901,61902,61903,61906],{},"Now for the most important part - actually calling the AI model to generate each image and saving it to our local machine. We'll create a function called ",[22,61904,61905],{},"generateImage"," that takes one set of parameters and produces one image:",[128,61908,61910],{"className":130,"code":61909,"language":132,"meta":133,"style":133},"async function generateImage(params: ImageParams) {\n    \u002F\u002F First, construct a descriptive filename based on the parameters\n    const filename = `image_pitch${params.rotate_pitch}_px${params.pupil_x}_py${params.pupil_y}.webp`;\n    const filepath = path.join(\"generated-images\", filename);\n\n    \u002F\u002F If this image already exists, skip it so we don't waste API credits re-generating it\n    if (fs.existsSync(filepath)) {\n        console.log(\"Skipping existing:\", filename);\n        return;\n    }\n\n    console.log(\"Generating:\", filename);\n\n    \u002F\u002F Call the Replicate API to run the expression-editor model\n    const output = await replicate.run(\n        \"fofr\u002Fexpression-editor:bf913bc90e1c44ba288ba3942a538693b72e8cc7df576f3beebe56adc0a92b86\",\n        {\n            input: {\n                image: IMAGE_URL,\n                rotate_pitch: params.rotate_pitch,\n                pupil_x: params.pupil_x,\n                pupil_y: params.pupil_y,\n\n                \u002F\u002F Keep all other facial expression parameters at neutral\u002Fzero\n                \u002F\u002F so we don't accidentally add smiles, winks, etc.\n                aaa: 0,\n                eee: 0,\n                woo: 0,\n                wink: 0,\n                blink: 0,\n                smile: 0,\n                eyebrow: 0,\n                src_ratio: 1,\n                rotate_yaw: 0,\n                rotate_roll: 0,\n                crop_factor: 2.5,\n                sample_ratio: 1,\n                output_format: \"webp\",\n                output_quality: 95,\n            },\n        }\n    );\n\n    \u002F\u002F The model returns an array with one file object\n    \u002F\u002F We need to convert it to a buffer and save it to disk\n    const file = output[0] as any;\n    const buffer = Buffer.from(await file.arrayBuffer());\n    fs.writeFileSync(filepath, buffer);\n}\n",[22,61911,61912,61932,61937,61978,62000,62004,62009,62020,62033,62039,62043,62047,62060,62064,62069,62087,62094,62098,62103,62113,62118,62123,62128,62132,62137,62142,62151,62160,62169,62178,62187,62196,62205,62214,62223,62232,62242,62251,62261,62271,62275,62279,62283,62287,62292,62297,62318,62341,62350],{"__ignoreMap":133},[137,61913,61914,61916,61918,61921,61923,61926,61928,61930],{"class":139,"line":140},[137,61915,15050],{"class":143},[137,61917,154],{"class":143},[137,61919,61920],{"class":147}," generateImage",[137,61922,356],{"class":157},[137,61924,61925],{"class":161},"params",[137,61927,894],{"class":143},[137,61929,61720],{"class":147},[137,61931,170],{"class":157},[137,61933,61934],{"class":139,"line":173},[137,61935,61936],{"class":308},"    \u002F\u002F First, construct a descriptive filename based on the parameters\n",[137,61938,61939,61941,61944,61946,61949,61951,61953,61955,61958,61960,61962,61964,61967,61969,61971,61973,61976],{"class":139,"line":188},[137,61940,4177],{"class":143},[137,61942,61943],{"class":364}," filename",[137,61945,151],{"class":143},[137,61947,61948],{"class":284}," `image_pitch${",[137,61950,61925],{"class":157},[137,61952,1017],{"class":284},[137,61954,61275],{"class":157},[137,61956,61957],{"class":284},"}_px${",[137,61959,61925],{"class":157},[137,61961,1017],{"class":284},[137,61963,61285],{"class":157},[137,61965,61966],{"class":284},"}_py${",[137,61968,61925],{"class":157},[137,61970,1017],{"class":284},[137,61972,61295],{"class":157},[137,61974,61975],{"class":284},"}.webp`",[137,61977,3276],{"class":157},[137,61979,61980,61982,61985,61987,61990,61992,61994,61997],{"class":139,"line":269},[137,61981,4177],{"class":143},[137,61983,61984],{"class":364}," filepath",[137,61986,151],{"class":143},[137,61988,61989],{"class":157}," path.",[137,61991,8628],{"class":147},[137,61993,356],{"class":157},[137,61995,61996],{"class":284},"\"generated-images\"",[137,61998,61999],{"class":157},", filename);\n",[137,62001,62002],{"class":139,"line":278},[137,62003,516],{"emptyLinePlaceholder":515},[137,62005,62006],{"class":139,"line":291},[137,62007,62008],{"class":308},"    \u002F\u002F If this image already exists, skip it so we don't waste API credits re-generating it\n",[137,62010,62011,62013,62015,62017],{"class":139,"line":297},[137,62012,24696],{"class":143},[137,62014,48619],{"class":157},[137,62016,48622],{"class":147},[137,62018,62019],{"class":157},"(filepath)) {\n",[137,62021,62022,62024,62026,62028,62031],{"class":139,"line":302},[137,62023,350],{"class":157},[137,62025,353],{"class":147},[137,62027,356],{"class":157},[137,62029,62030],{"class":284},"\"Skipping existing:\"",[137,62032,61999],{"class":157},[137,62034,62035,62037],{"class":139,"line":662},[137,62036,5472],{"class":143},[137,62038,3276],{"class":157},[137,62040,62041],{"class":139,"line":667},[137,62042,294],{"class":157},[137,62044,62045],{"class":139,"line":786},[137,62046,516],{"emptyLinePlaceholder":515},[137,62048,62049,62051,62053,62055,62058],{"class":139,"line":798},[137,62050,493],{"class":157},[137,62052,353],{"class":147},[137,62054,356],{"class":157},[137,62056,62057],{"class":284},"\"Generating:\"",[137,62059,61999],{"class":157},[137,62061,62062],{"class":139,"line":803},[137,62063,516],{"emptyLinePlaceholder":515},[137,62065,62066],{"class":139,"line":931},[137,62067,62068],{"class":308},"    \u002F\u002F Call the Replicate API to run the expression-editor model\n",[137,62070,62071,62073,62075,62077,62079,62082,62085],{"class":139,"line":1568},[137,62072,4177],{"class":143},[137,62074,46353],{"class":364},[137,62076,151],{"class":143},[137,62078,15069],{"class":143},[137,62080,62081],{"class":157}," replicate.",[137,62083,62084],{"class":147},"run",[137,62086,11813],{"class":157},[137,62088,62089,62092],{"class":139,"line":1573},[137,62090,62091],{"class":284},"        \"fofr\u002Fexpression-editor:bf913bc90e1c44ba288ba3942a538693b72e8cc7df576f3beebe56adc0a92b86\"",[137,62093,1961],{"class":157},[137,62095,62096],{"class":139,"line":1578},[137,62097,11825],{"class":157},[137,62099,62100],{"class":139,"line":1588},[137,62101,62102],{"class":157},"            input: {\n",[137,62104,62105,62108,62111],{"class":139,"line":1601},[137,62106,62107],{"class":157},"                image: ",[137,62109,62110],{"class":364},"IMAGE_URL",[137,62112,1961],{"class":157},[137,62114,62115],{"class":139,"line":3802},[137,62116,62117],{"class":157},"                rotate_pitch: params.rotate_pitch,\n",[137,62119,62120],{"class":139,"line":3808},[137,62121,62122],{"class":157},"                pupil_x: params.pupil_x,\n",[137,62124,62125],{"class":139,"line":3822},[137,62126,62127],{"class":157},"                pupil_y: params.pupil_y,\n",[137,62129,62130],{"class":139,"line":3827},[137,62131,516],{"emptyLinePlaceholder":515},[137,62133,62134],{"class":139,"line":3832},[137,62135,62136],{"class":308},"                \u002F\u002F Keep all other facial expression parameters at neutral\u002Fzero\n",[137,62138,62139],{"class":139,"line":3840},[137,62140,62141],{"class":308},"                \u002F\u002F so we don't accidentally add smiles, winks, etc.\n",[137,62143,62144,62147,62149],{"class":139,"line":3846},[137,62145,62146],{"class":157},"                aaa: ",[137,62148,6044],{"class":364},[137,62150,1961],{"class":157},[137,62152,62153,62156,62158],{"class":139,"line":3861},[137,62154,62155],{"class":157},"                eee: ",[137,62157,6044],{"class":364},[137,62159,1961],{"class":157},[137,62161,62162,62165,62167],{"class":139,"line":3883},[137,62163,62164],{"class":157},"                woo: ",[137,62166,6044],{"class":364},[137,62168,1961],{"class":157},[137,62170,62171,62174,62176],{"class":139,"line":3896},[137,62172,62173],{"class":157},"                wink: ",[137,62175,6044],{"class":364},[137,62177,1961],{"class":157},[137,62179,62180,62183,62185],{"class":139,"line":3901},[137,62181,62182],{"class":157},"                blink: ",[137,62184,6044],{"class":364},[137,62186,1961],{"class":157},[137,62188,62189,62192,62194],{"class":139,"line":3906},[137,62190,62191],{"class":157},"                smile: ",[137,62193,6044],{"class":364},[137,62195,1961],{"class":157},[137,62197,62198,62201,62203],{"class":139,"line":3911},[137,62199,62200],{"class":157},"                eyebrow: ",[137,62202,6044],{"class":364},[137,62204,1961],{"class":157},[137,62206,62207,62210,62212],{"class":139,"line":4666},[137,62208,62209],{"class":157},"                src_ratio: ",[137,62211,6065],{"class":364},[137,62213,1961],{"class":157},[137,62215,62216,62219,62221],{"class":139,"line":4672},[137,62217,62218],{"class":157},"                rotate_yaw: ",[137,62220,6044],{"class":364},[137,62222,1961],{"class":157},[137,62224,62225,62228,62230],{"class":139,"line":4680},[137,62226,62227],{"class":157},"                rotate_roll: ",[137,62229,6044],{"class":364},[137,62231,1961],{"class":157},[137,62233,62234,62237,62240],{"class":139,"line":4711},[137,62235,62236],{"class":157},"                crop_factor: ",[137,62238,62239],{"class":364},"2.5",[137,62241,1961],{"class":157},[137,62243,62244,62247,62249],{"class":139,"line":4716},[137,62245,62246],{"class":157},"                sample_ratio: ",[137,62248,6065],{"class":364},[137,62250,1961],{"class":157},[137,62252,62253,62256,62259],{"class":139,"line":4721},[137,62254,62255],{"class":157},"                output_format: ",[137,62257,62258],{"class":284},"\"webp\"",[137,62260,1961],{"class":157},[137,62262,62263,62266,62269],{"class":139,"line":4727},[137,62264,62265],{"class":157},"                output_quality: ",[137,62267,62268],{"class":364},"95",[137,62270,1961],{"class":157},[137,62272,62273],{"class":139,"line":4732},[137,62274,14074],{"class":157},[137,62276,62277],{"class":139,"line":5006},[137,62278,1966],{"class":157},[137,62280,62281],{"class":139,"line":5014},[137,62282,11875],{"class":157},[137,62284,62285],{"class":139,"line":14343},[137,62286,516],{"emptyLinePlaceholder":515},[137,62288,62289],{"class":139,"line":24199},[137,62290,62291],{"class":308},"    \u002F\u002F The model returns an array with one file object\n",[137,62293,62294],{"class":139,"line":24773},[137,62295,62296],{"class":308},"    \u002F\u002F We need to convert it to a buffer and save it to disk\n",[137,62298,62299,62301,62303,62305,62308,62310,62312,62314,62316],{"class":139,"line":24778},[137,62300,4177],{"class":143},[137,62302,22296],{"class":364},[137,62304,151],{"class":143},[137,62306,62307],{"class":157}," output[",[137,62309,6044],{"class":364},[137,62311,5796],{"class":157},[137,62313,24431],{"class":143},[137,62315,26137],{"class":364},[137,62317,3276],{"class":157},[137,62319,62320,62322,62325,62327,62329,62331,62333,62335,62337,62339],{"class":139,"line":24783},[137,62321,4177],{"class":143},[137,62323,62324],{"class":364}," buffer",[137,62326,151],{"class":143},[137,62328,57793],{"class":157},[137,62330,10954],{"class":147},[137,62332,356],{"class":157},[137,62334,54992],{"class":143},[137,62336,10277],{"class":157},[137,62338,57805],{"class":147},[137,62340,14173],{"class":157},[137,62342,62343,62345,62347],{"class":139,"line":24793},[137,62344,58174],{"class":157},[137,62346,58177],{"class":147},[137,62348,62349],{"class":157},"(filepath, buffer);\n",[137,62351,62352],{"class":139,"line":24827},[137,62353,510],{"class":157},[27,62355,62356,62357,62360,62361,62364,62365,62368,62369,4409],{},"First, we construct a filename that describes the parameters used - this makes it easy to identify which image is which later. For example, an image might be named ",[22,62358,62359],{},"image_pitch-10_px15_py0.webp",", which tells us it has the head tilted slightly down (",[22,62362,62363],{},"pitch -10","), eyes looking right (",[22,62366,62367],{},"pupil_x 15","), and eyes centred vertically (",[22,62370,62371],{},"pupil_y 0",[27,62373,62374],{},"Before we generate anything, we check if this file already exists. This is really important because if you need to stop and restart your script for any reason, you don't want to waste time and API credits regenerating images you already have.",[27,62376,62377,62378,62381,62382,164,62384,164,62386,62388],{},"Then we call ",[22,62379,62380],{},"replicate.run()"," with the model ID and all our parameters. Notice how we're setting our three key parameters (",[22,62383,61275],{},[22,62385,61285],{},[22,62387,61295],{},") to the values we want, but we're also explicitly setting all the other expression parameters to 0 or their neutral values. This ensures we get a neutral expression with just the head and eye positions we want - no accidental smiles or winks.",[27,62390,62391,62392,62395],{},"Finally, we take the output from the model, convert it to a binary buffer, and write it to a file in our ",[22,62393,62394],{},"generated-images"," folder.",[123,62397,62399],{"id":62398},"wiring-it-all-together-in-the-main-function","Wiring It All Together in the Main Function",[27,62401,62402],{},"Now let's create our main function that ties everything together and actually runs the generation process:",[128,62404,62406],{"className":13299,"code":62405,"language":13301,"meta":133,"style":133},"async function main() {\n    \u002F\u002F Create the output folder if it doesn't exist yet\n    if (!fs.existsSync(\"generated-images\")) {\n        fs.mkdirSync(\"generated-images\");\n    }\n\n    \u002F\u002F Get all the combinations we need to generate\n    const combos = getCombinations();\n\n    \u002F\u002F Loop through each combination and generate its image\n    for (const combo of combos) {\n        try {\n            await generateImage(combo);\n            \u002F\u002F Add a small delay between requests to avoid hitting rate limits\n            await new Promise((r) => setTimeout(r, 3000));\n        } catch (e) {\n            console.error(\"Error generating image\", combo, e);\n        }\n    }\n}\n\nmain().catch(console.error);\n",[22,62407,62408,62418,62423,62441,62454,62458,62462,62467,62479,62483,62488,62504,62510,62519,62524,62551,62560,62574,62578,62582,62586,62590],{"__ignoreMap":133},[137,62409,62410,62412,62414,62416],{"class":139,"line":140},[137,62411,15050],{"class":143},[137,62413,154],{"class":143},[137,62415,58463],{"class":147},[137,62417,275],{"class":157},[137,62419,62420],{"class":139,"line":173},[137,62421,62422],{"class":308},"    \u002F\u002F Create the output folder if it doesn't exist yet\n",[137,62424,62425,62427,62429,62431,62433,62435,62437,62439],{"class":139,"line":188},[137,62426,24696],{"class":143},[137,62428,158],{"class":157},[137,62430,17393],{"class":143},[137,62432,58568],{"class":157},[137,62434,48622],{"class":147},[137,62436,356],{"class":157},[137,62438,61996],{"class":284},[137,62440,34157],{"class":157},[137,62442,62443,62445,62448,62450,62452],{"class":139,"line":269},[137,62444,48630],{"class":157},[137,62446,62447],{"class":147},"mkdirSync",[137,62449,356],{"class":157},[137,62451,61996],{"class":284},[137,62453,1502],{"class":157},[137,62455,62456],{"class":139,"line":278},[137,62457,294],{"class":157},[137,62459,62460],{"class":139,"line":291},[137,62461,516],{"emptyLinePlaceholder":515},[137,62463,62464],{"class":139,"line":297},[137,62465,62466],{"class":308},"    \u002F\u002F Get all the combinations we need to generate\n",[137,62468,62469,62471,62473,62475,62477],{"class":139,"line":302},[137,62470,4177],{"class":143},[137,62472,61787],{"class":364},[137,62474,151],{"class":143},[137,62476,61770],{"class":147},[137,62478,924],{"class":157},[137,62480,62481],{"class":139,"line":662},[137,62482,516],{"emptyLinePlaceholder":515},[137,62484,62485],{"class":139,"line":667},[137,62486,62487],{"class":308},"    \u002F\u002F Loop through each combination and generate its image\n",[137,62489,62490,62492,62494,62496,62499,62501],{"class":139,"line":786},[137,62491,21473],{"class":143},[137,62493,158],{"class":157},[137,62495,3077],{"class":143},[137,62497,62498],{"class":364}," combo",[137,62500,48913],{"class":143},[137,62502,62503],{"class":157}," combos) {\n",[137,62505,62506,62508],{"class":139,"line":798},[137,62507,15697],{"class":143},[137,62509,256],{"class":157},[137,62511,62512,62514,62516],{"class":139,"line":803},[137,62513,18839],{"class":143},[137,62515,61920],{"class":147},[137,62517,62518],{"class":157},"(combo);\n",[137,62520,62521],{"class":139,"line":931},[137,62522,62523],{"class":308},"            \u002F\u002F Add a small delay between requests to avoid hitting rate limits\n",[137,62525,62526,62528,62530,62532,62534,62537,62539,62541,62544,62547,62549],{"class":139,"line":1568},[137,62527,18839],{"class":143},[137,62529,1426],{"class":143},[137,62531,14116],{"class":364},[137,62533,2774],{"class":157},[137,62535,62536],{"class":161},"r",[137,62538,219],{"class":157},[137,62540,222],{"class":143},[137,62542,62543],{"class":147}," setTimeout",[137,62545,62546],{"class":157},"(r, ",[137,62548,15111],{"class":364},[137,62550,8614],{"class":157},[137,62552,62553,62555,62557],{"class":139,"line":1573},[137,62554,15729],{"class":157},[137,62556,2807],{"class":143},[137,62558,62559],{"class":157}," (e) {\n",[137,62561,62562,62564,62566,62568,62571],{"class":139,"line":1578},[137,62563,1493],{"class":157},[137,62565,2812],{"class":147},[137,62567,356],{"class":157},[137,62569,62570],{"class":284},"\"Error generating image\"",[137,62572,62573],{"class":157},", combo, e);\n",[137,62575,62576],{"class":139,"line":1588},[137,62577,1966],{"class":157},[137,62579,62580],{"class":139,"line":1601},[137,62581,294],{"class":157},[137,62583,62584],{"class":139,"line":3802},[137,62585,510],{"class":157},[137,62587,62588],{"class":139,"line":3808},[137,62589,516],{"emptyLinePlaceholder":515},[137,62591,62592,62594,62596,62598],{"class":139,"line":3822},[137,62593,58927],{"class":147},[137,62595,17766],{"class":157},[137,62597,2807],{"class":147},[137,62599,62600],{"class":157},"(console.error);\n",[27,62602,62603,62604,62606,62607,62610,62611,62614],{},"This main function orchestrates the entire generation process. First, it makes sure we have a ",[22,62605,62394],{}," folder to store our output. Then it gets all 45 combinations from our ",[22,62608,62609],{},"getCombinations()"," function and loops through each one, calling ",[22,62612,62613],{},"generateImage()"," for each combination.",[27,62616,62617],{},"I've wrapped each generation call in a try-catch block so that if one image fails to generate for some reason, the script will log the error and continue with the rest. I've also added a 3-second delay between each API call - this is important to avoid hitting Replicate's rate limits.",[27,62619,62620],{},"Now you can run the script! In your terminal, type:",[128,62622,62624],{"className":8665,"code":62623,"language":8667,"meta":133,"style":133},"npm run start\n",[22,62625,62626],{"__ignoreMap":133},[137,62627,62628,62630,62632],{"class":139,"line":140},[137,62629,9536],{"class":147},[137,62631,9578],{"class":284},[137,62633,13062],{"class":284},[27,62635,62636],{},"This will take a while - probably 10 - 15 minutes to generate all 45 images. You'll see console messages as each image is generated. When it's done, you'll have a folder structure that looks like this:",[128,62638,62641],{"className":62639,"code":62640,"language":5189},[5187],"generated-images\u002F\n  image_pitch-20_px-15_py-15.webp\n  image_pitch-20_px-15_py0.webp\n  image_pitch-20_px-15_py15.webp\n  ...\n  image_pitch20_px15_py15.webp\n",[22,62642,62640],{"__ignoreMap":133},[27,62644,62645],{},"This is essentially our \"sprite sheet\" - except instead of being packed into one big image file, we have each frame as a separate file, which makes it easier to work with in the browser.",[104,62647,62649],{"id":62648},"building-the-basic-html-structure","Building the Basic HTML Structure",[27,62651,62652],{},"Now that we have all our generated face images, it's time to build the web page that will display them and make the face follow the cursor. The front-end is surprisingly simple - it's just a single HTML file with a bit of JavaScript embedded in it. The HTML file needs:",[1003,62654,62655,62658],{},[1006,62656,62657],{},"A container element where all our face images will be stacked on top of each other.",[1006,62659,62660,62661],{},"Some JavaScript code that:\n",[1003,62662,62663,62666,62669,62672],{},[1006,62664,62665],{},"Preloads all 45 images so they're ready to display instantly.",[1006,62667,62668],{},"Tracks the user's mouse movement across the screen.",[1006,62670,62671],{},"Figures out which image best matches the current mouse position.",[1006,62673,62674],{},"Shows only that one image while hiding all the others.",[27,62676,62677,62678,62680],{},"Let's start with the absolute bare minimum HTML structure. Create a new file called ",[22,62679,23204],{}," in your project folder and add this code:",[128,62682,62684],{"className":4024,"code":62683,"language":4026,"meta":133,"style":133},"\u003C!DOCTYPE html>\n\u003Chtml lang=\"en\">\n    \u003Chead>\n        \u003Cmeta charset=\"UTF-8\" \u002F>\n        \u003Ctitle>Eye Tracking Face\u003C\u002Ftitle>\n        \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" \u002F>\n    \u003C\u002Fhead>\n    \u003Cbody>\n        \u003Cdiv id=\"loading\">\n            \u003Cdiv id=\"progress-text\">Loading images... 0%\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n\n        \u003Cdiv id=\"instructions\">Move your mouse around (or tilt your phone) to make the face follow you.\u003C\u002Fdiv>\n\n        \u003Cbutton id=\"motion-button\">Enable Motion Tracking\u003C\u002Fbutton>\n\n        \u003Cdiv id=\"image-container\">\u003C\u002Fdiv>\n\n        \u003Cscript>\n            const pitchValues = [-20, -10, 0, 10, 20];\n            const pupilXValues = [-15, 0, 15];\n            const pupilYValues = [-15, 0, 15];\n\n            const images = [];\n            for (const pitch of pitchValues) {\n                for (const pupil_x of pupilXValues) {\n                    for (const pupil_y of pupilYValues) {\n                        const filename = `image_pitch${pitch}_px${pupil_x}_py${pupil_y}.webp`;\n                        images.push({ pitch, pupil_x, pupil_y, filename });\n                    }\n                }\n            }\n\n            const container = document.getElementById(\"image-container\");\n            const loading = document.getElementById(\"loading\");\n            const progressText = document.getElementById(\"progress-text\");\n            const motionButton = document.getElementById(\"motion-button\");\n\n            const imageElements = new Map();\n            let loadedCount = 0;\n\n            \u002F\u002F Preload all images\n            images.forEach((imgInfo) => {\n                const img = new Image();\n                img.src = `.\u002Fgenerated-images\u002F${imgInfo.filename}`;\n                img.onload = () => {\n                    loadedCount++;\n                    progressText.textContent = `Loading images... ${Math.round((loadedCount \u002F images.length) * 100)}%`;\n                    if (loadedCount === images.length) {\n                        loading.classList.add(\"hidden\");\n                        \u002F\u002F Show center image initially\n                        showClosestImage(0, 0);\n                    }\n                };\n                img.onerror = () => {\n                    loadedCount++;\n                };\n\n                img.dataset.pitch = imgInfo.pitch;\n                img.dataset.pupilX = imgInfo.pupil_x;\n                img.dataset.pupilY = imgInfo.pupil_y;\n\n                container.appendChild(img);\n                imageElements.set(imgInfo.filename, img);\n            });\n\n            let currentActive = null;\n\n            function showImageByInfo(info) {\n                const filename = info.filename;\n                const img = imageElements.get(filename);\n                if (!img) return;\n\n                if (currentActive) {\n                    currentActive.classList.remove(\"active\");\n                }\n                img.classList.add(\"active\");\n                currentActive = img;\n            }\n\n            \u002F\u002F Helper: map [-1, 1] to nearest value in array\n            function nearestInRange(value, values) {\n                const min = values[0];\n                const max = values[values.length - 1];\n                const mapped = min + ((value + 1) \u002F 2) * (max - min);\n                let best = values[0];\n                let bestDist = Infinity;\n                for (const v of values) {\n                    const d = Math.abs(v - mapped);\n                    if (d \u003C bestDist) {\n                        bestDist = d;\n                        best = v;\n                    }\n                }\n                return best;\n            }\n\n            function showClosestImage(normX, normY) {\n                \u002F\u002F normX, normY are from -1 to 1\n                const pupil_x = nearestInRange(normX, pupilXValues);\n                const pupil_y = nearestInRange(-normY, pupilYValues); \u002F\u002F invert so up = negative\n                const pitch = nearestInRange(-normY, pitchValues);\n\n                const match = images.find(\n                    (img) => img.pupil_x === pupil_x && img.pupil_y === pupil_y && img.pitch === pitch\n                );\n                if (match) {\n                    showImageByInfo(match);\n                }\n            }\n\n            \u002F\u002F Mouse tracking (desktop)\n            window.addEventListener(\"mousemove\", (e) => {\n                const rect = container.getBoundingClientRect();\n                const cx = rect.left + rect.width \u002F 2;\n                const cy = rect.top + rect.height \u002F 2;\n\n                \u002F\u002F -1..1 based on distance from center\n                let normX = (e.clientX - cx) \u002F (window.innerWidth \u002F 2);\n                let normY = (e.clientY - cy) \u002F (window.innerHeight \u002F 2);\n\n                normX = Math.max(-1, Math.min(1, normX));\n                normY = Math.max(-1, Math.min(1, normY));\n\n                showClosestImage(normX, normY);\n            });\n\n            \u002F\u002F Device motion (mobile)\n            function isMobile() {\n                return \u002FAndroid|iPhone|iPad|iPod\u002Fi.test(navigator.userAgent);\n            }\n\n            async function enableMotion() {\n                try {\n                    if (\n                        typeof DeviceOrientationEvent !== \"undefined\" &&\n                        typeof DeviceOrientationEvent.requestPermission === \"function\"\n                    ) {\n                        const perm = await DeviceOrientationEvent.requestPermission();\n                        if (perm !== \"granted\") return;\n                    }\n\n                    window.addEventListener(\"deviceorientation\", (event) => {\n                        const beta = event.beta || 0; \u002F\u002F front\u002Fback tilt (-180..180)\n                        const gamma = event.gamma || 0; \u002F\u002F left\u002Fright tilt (-90..90)\n\n                        \u002F\u002F Map tilt to -1..1 range\n                        const normX = Math.max(-1, Math.min(1, gamma \u002F 45)); \u002F\u002F left\u002Fright\n                        const normY = Math.max(-1, Math.min(1, beta \u002F 45)); \u002F\u002F up\u002Fdown\n\n                        showClosestImage(normX, normY);\n                    });\n\n                    motionButton.classList.add(\"hidden\");\n                } catch (e) {\n                    console.error(e);\n                }\n            }\n\n            if (isMobile()) {\n                motionButton.classList.remove(\"hidden\");\n                motionButton.addEventListener(\"click\", enableMotion);\n            } else {\n                motionButton.classList.add(\"hidden\");\n            }\n        \u003C\u002Fscript>\n    \u003C\u002Fbody>\n\u003C\u002Fhtml>\n",[22,62685,62686,62696,62710,62718,62732,62745,62765,62773,62781,62796,62816,62824,62828,62848,62852,62872,62876,62895,62899,62907,62941,62965,62989,62993,63004,63018,63034,63050,63076,63086,63091,63095,63099,63103,63122,63141,63160,63179,63183,63199,63213,63217,63222,63240,63256,63277,63293,63302,63346,63363,63377,63382,63397,63401,63405,63419,63427,63431,63435,63445,63455,63465,63469,63480,63490,63494,63498,63511,63515,63529,63540,63556,63571,63575,63582,63595,63599,63612,63622,63626,63630,63635,63653,63669,63690,63729,63745,63759,63775,63798,63810,63821,63832,63837,63842,63850,63855,63860,63880,63886,63900,63921,63939,63944,63959,63999,64004,64012,64021,64026,64031,64036,64042,64065,64083,64106,64129,64134,64140,64169,64198,64203,64235,64264,64269,64278,64283,64288,64294,64304,64336,64341,64346,64359,64367,64374,64391,64404,64410,64430,64449,64454,64459,64482,64504,64526,64531,64537,64579,64619,64624,64631,64637,64642,64656,64666,64677,64682,64687,64692,64705,64719,64734,64743,64756,64761,64770,64779],{"__ignoreMap":133},[137,62687,62688,62690,62692,62694],{"class":139,"line":140},[137,62689,25574],{"class":157},[137,62691,59401],{"class":4036},[137,62693,25580],{"class":147},[137,62695,4053],{"class":157},[137,62697,62698,62700,62702,62704,62706,62708],{"class":139,"line":173},[137,62699,4033],{"class":157},[137,62701,4026],{"class":4036},[137,62703,25591],{"class":147},[137,62705,253],{"class":157},[137,62707,25596],{"class":284},[137,62709,4053],{"class":157},[137,62711,62712,62714,62716],{"class":139,"line":188},[137,62713,4072],{"class":157},[137,62715,25605],{"class":4036},[137,62717,4053],{"class":157},[137,62719,62720,62722,62724,62726,62728,62730],{"class":139,"line":269},[137,62721,9826],{"class":157},[137,62723,23508],{"class":4036},[137,62725,25616],{"class":147},[137,62727,253],{"class":157},[137,62729,25621],{"class":284},[137,62731,4078],{"class":157},[137,62733,62734,62736,62738,62741,62743],{"class":139,"line":278},[137,62735,9826],{"class":157},[137,62737,25683],{"class":4036},[137,62739,62740],{"class":157},">Eye Tracking Face\u003C\u002F",[137,62742,25683],{"class":4036},[137,62744,4053],{"class":157},[137,62746,62747,62749,62751,62753,62755,62757,62759,62761,62763],{"class":139,"line":291},[137,62748,9826],{"class":157},[137,62750,23508],{"class":4036},[137,62752,891],{"class":147},[137,62754,253],{"class":157},[137,62756,25666],{"class":284},[137,62758,25669],{"class":147},[137,62760,253],{"class":157},[137,62762,25674],{"class":284},[137,62764,4078],{"class":157},[137,62766,62767,62769,62771],{"class":139,"line":297},[137,62768,8374],{"class":157},[137,62770,25605],{"class":4036},[137,62772,4053],{"class":157},[137,62774,62775,62777,62779],{"class":139,"line":302},[137,62776,4072],{"class":157},[137,62778,4065],{"class":4036},[137,62780,4053],{"class":157},[137,62782,62783,62785,62787,62789,62791,62794],{"class":139,"line":662},[137,62784,9826],{"class":157},[137,62786,8330],{"class":4036},[137,62788,23757],{"class":147},[137,62790,253],{"class":157},[137,62792,62793],{"class":284},"\"loading\"",[137,62795,4053],{"class":157},[137,62797,62798,62800,62802,62804,62806,62809,62812,62814],{"class":139,"line":667},[137,62799,23852],{"class":157},[137,62801,8330],{"class":4036},[137,62803,23757],{"class":147},[137,62805,253],{"class":157},[137,62807,62808],{"class":284},"\"progress-text\"",[137,62810,62811],{"class":157},">Loading images... 0%\u003C\u002F",[137,62813,8330],{"class":4036},[137,62815,4053],{"class":157},[137,62817,62818,62820,62822],{"class":139,"line":786},[137,62819,9843],{"class":157},[137,62821,8330],{"class":4036},[137,62823,4053],{"class":157},[137,62825,62826],{"class":139,"line":798},[137,62827,516],{"emptyLinePlaceholder":515},[137,62829,62830,62832,62834,62836,62838,62841,62844,62846],{"class":139,"line":803},[137,62831,9826],{"class":157},[137,62833,8330],{"class":4036},[137,62835,23757],{"class":147},[137,62837,253],{"class":157},[137,62839,62840],{"class":284},"\"instructions\"",[137,62842,62843],{"class":157},">Move your mouse around (or tilt your phone) to make the face follow you.\u003C\u002F",[137,62845,8330],{"class":4036},[137,62847,4053],{"class":157},[137,62849,62850],{"class":139,"line":931},[137,62851,516],{"emptyLinePlaceholder":515},[137,62853,62854,62856,62858,62860,62862,62865,62868,62870],{"class":139,"line":1568},[137,62855,9826],{"class":157},[137,62857,8170],{"class":4036},[137,62859,23757],{"class":147},[137,62861,253],{"class":157},[137,62863,62864],{"class":284},"\"motion-button\"",[137,62866,62867],{"class":157},">Enable Motion Tracking\u003C\u002F",[137,62869,8170],{"class":4036},[137,62871,4053],{"class":157},[137,62873,62874],{"class":139,"line":1573},[137,62875,516],{"emptyLinePlaceholder":515},[137,62877,62878,62880,62882,62884,62886,62889,62891,62893],{"class":139,"line":1578},[137,62879,9826],{"class":157},[137,62881,8330],{"class":4036},[137,62883,23757],{"class":147},[137,62885,253],{"class":157},[137,62887,62888],{"class":284},"\"image-container\"",[137,62890,4048],{"class":157},[137,62892,8330],{"class":4036},[137,62894,4053],{"class":157},[137,62896,62897],{"class":139,"line":1588},[137,62898,516],{"emptyLinePlaceholder":515},[137,62900,62901,62903,62905],{"class":139,"line":1601},[137,62902,9826],{"class":157},[137,62904,4037],{"class":4036},[137,62906,4053],{"class":157},[137,62908,62909,62911,62913,62915,62917,62919,62921,62923,62925,62927,62929,62931,62933,62935,62937,62939],{"class":139,"line":3802},[137,62910,5772],{"class":143},[137,62912,61497],{"class":364},[137,62914,151],{"class":143},[137,62916,22130],{"class":157},[137,62918,8215],{"class":143},[137,62920,14727],{"class":364},[137,62922,164],{"class":157},[137,62924,8215],{"class":143},[137,62926,33489],{"class":364},[137,62928,164],{"class":157},[137,62930,6044],{"class":364},[137,62932,164],{"class":157},[137,62934,33489],{"class":364},[137,62936,164],{"class":157},[137,62938,14727],{"class":364},[137,62940,5727],{"class":157},[137,62942,62943,62945,62947,62949,62951,62953,62955,62957,62959,62961,62963],{"class":139,"line":3808},[137,62944,5772],{"class":143},[137,62946,61532],{"class":364},[137,62948,151],{"class":143},[137,62950,22130],{"class":157},[137,62952,8215],{"class":143},[137,62954,61541],{"class":364},[137,62956,164],{"class":157},[137,62958,6044],{"class":364},[137,62960,164],{"class":157},[137,62962,61541],{"class":364},[137,62964,5727],{"class":157},[137,62966,62967,62969,62971,62973,62975,62977,62979,62981,62983,62985,62987],{"class":139,"line":3822},[137,62968,5772],{"class":143},[137,62970,61558],{"class":364},[137,62972,151],{"class":143},[137,62974,22130],{"class":157},[137,62976,8215],{"class":143},[137,62978,61541],{"class":364},[137,62980,164],{"class":157},[137,62982,6044],{"class":364},[137,62984,164],{"class":157},[137,62986,61541],{"class":364},[137,62988,5727],{"class":157},[137,62990,62991],{"class":139,"line":3827},[137,62992,516],{"emptyLinePlaceholder":515},[137,62994,62995,62997,63000,63002],{"class":139,"line":3832},[137,62996,5772],{"class":143},[137,62998,62999],{"class":364}," images",[137,63001,151],{"class":143},[137,63003,8556],{"class":157},[137,63005,63006,63008,63010,63012,63014,63016],{"class":139,"line":3840},[137,63007,61834],{"class":143},[137,63009,158],{"class":157},[137,63011,3077],{"class":143},[137,63013,61808],{"class":364},[137,63015,48913],{"class":143},[137,63017,61813],{"class":157},[137,63019,63020,63023,63025,63027,63030,63032],{"class":139,"line":3846},[137,63021,63022],{"class":143},"                for",[137,63024,158],{"class":157},[137,63026,3077],{"class":143},[137,63028,63029],{"class":364}," pupil_x",[137,63031,48913],{"class":143},[137,63033,61829],{"class":157},[137,63035,63036,63039,63041,63043,63046,63048],{"class":139,"line":3861},[137,63037,63038],{"class":143},"                    for",[137,63040,158],{"class":157},[137,63042,3077],{"class":143},[137,63044,63045],{"class":364}," pupil_y",[137,63047,48913],{"class":143},[137,63049,61846],{"class":157},[137,63051,63052,63055,63057,63059,63061,63064,63066,63068,63070,63072,63074],{"class":139,"line":3883},[137,63053,63054],{"class":143},"                        const",[137,63056,61943],{"class":364},[137,63058,151],{"class":143},[137,63060,61948],{"class":284},[137,63062,63063],{"class":157},"pitch",[137,63065,61957],{"class":284},[137,63067,61285],{"class":157},[137,63069,61966],{"class":284},[137,63071,61295],{"class":157},[137,63073,61975],{"class":284},[137,63075,3276],{"class":157},[137,63077,63078,63081,63083],{"class":139,"line":3896},[137,63079,63080],{"class":157},"                        images.",[137,63082,8583],{"class":147},[137,63084,63085],{"class":157},"({ pitch, pupil_x, pupil_y, filename });\n",[137,63087,63088],{"class":139,"line":3901},[137,63089,63090],{"class":157},"                    }\n",[137,63092,63093],{"class":139,"line":3906},[137,63094,34372],{"class":157},[137,63096,63097],{"class":139,"line":3911},[137,63098,760],{"class":157},[137,63100,63101],{"class":139,"line":4666},[137,63102,516],{"emptyLinePlaceholder":515},[137,63104,63105,63107,63110,63112,63114,63116,63118,63120],{"class":139,"line":4672},[137,63106,5772],{"class":143},[137,63108,63109],{"class":364}," container",[137,63111,151],{"class":143},[137,63113,3717],{"class":157},[137,63115,24422],{"class":147},[137,63117,356],{"class":157},[137,63119,62888],{"class":284},[137,63121,1502],{"class":157},[137,63123,63124,63126,63129,63131,63133,63135,63137,63139],{"class":139,"line":4680},[137,63125,5772],{"class":143},[137,63127,63128],{"class":364}," loading",[137,63130,151],{"class":143},[137,63132,3717],{"class":157},[137,63134,24422],{"class":147},[137,63136,356],{"class":157},[137,63138,62793],{"class":284},[137,63140,1502],{"class":157},[137,63142,63143,63145,63148,63150,63152,63154,63156,63158],{"class":139,"line":4711},[137,63144,5772],{"class":143},[137,63146,63147],{"class":364}," progressText",[137,63149,151],{"class":143},[137,63151,3717],{"class":157},[137,63153,24422],{"class":147},[137,63155,356],{"class":157},[137,63157,62808],{"class":284},[137,63159,1502],{"class":157},[137,63161,63162,63164,63167,63169,63171,63173,63175,63177],{"class":139,"line":4716},[137,63163,5772],{"class":143},[137,63165,63166],{"class":364}," motionButton",[137,63168,151],{"class":143},[137,63170,3717],{"class":157},[137,63172,24422],{"class":147},[137,63174,356],{"class":157},[137,63176,62864],{"class":284},[137,63178,1502],{"class":157},[137,63180,63181],{"class":139,"line":4721},[137,63182,516],{"emptyLinePlaceholder":515},[137,63184,63185,63187,63190,63192,63194,63197],{"class":139,"line":4727},[137,63186,5772],{"class":143},[137,63188,63189],{"class":364}," imageElements",[137,63191,151],{"class":143},[137,63193,1426],{"class":143},[137,63195,63196],{"class":147}," Map",[137,63198,924],{"class":157},[137,63200,63201,63204,63207,63209,63211],{"class":139,"line":4732},[137,63202,63203],{"class":143},"            let",[137,63205,63206],{"class":157}," loadedCount ",[137,63208,253],{"class":143},[137,63210,7687],{"class":364},[137,63212,3276],{"class":157},[137,63214,63215],{"class":139,"line":5006},[137,63216,516],{"emptyLinePlaceholder":515},[137,63218,63219],{"class":139,"line":5014},[137,63220,63221],{"class":308},"            \u002F\u002F Preload all images\n",[137,63223,63224,63227,63229,63231,63234,63236,63238],{"class":139,"line":14343},[137,63225,63226],{"class":157},"            images.",[137,63228,8564],{"class":147},[137,63230,2774],{"class":157},[137,63232,63233],{"class":161},"imgInfo",[137,63235,219],{"class":157},[137,63237,222],{"class":143},[137,63239,256],{"class":157},[137,63241,63242,63244,63247,63249,63251,63254],{"class":139,"line":24199},[137,63243,34330],{"class":143},[137,63245,63246],{"class":364}," img",[137,63248,151],{"class":143},[137,63250,1426],{"class":143},[137,63252,63253],{"class":147}," Image",[137,63255,924],{"class":157},[137,63257,63258,63261,63263,63266,63268,63270,63273,63275],{"class":139,"line":24773},[137,63259,63260],{"class":157},"                img.src ",[137,63262,253],{"class":143},[137,63264,63265],{"class":284}," `.\u002Fgenerated-images\u002F${",[137,63267,63233],{"class":157},[137,63269,1017],{"class":284},[137,63271,63272],{"class":157},"filename",[137,63274,4706],{"class":284},[137,63276,3276],{"class":157},[137,63278,63279,63282,63285,63287,63289,63291],{"class":139,"line":24778},[137,63280,63281],{"class":157},"                img.",[137,63283,63284],{"class":147},"onload",[137,63286,151],{"class":143},[137,63288,1484],{"class":157},[137,63290,222],{"class":143},[137,63292,256],{"class":157},[137,63294,63295,63298,63300],{"class":139,"line":24783},[137,63296,63297],{"class":157},"                    loadedCount",[137,63299,12683],{"class":143},[137,63301,3276],{"class":157},[137,63303,63304,63307,63309,63312,63315,63317,63320,63322,63325,63327,63329,63331,63333,63335,63337,63339,63341,63344],{"class":139,"line":24793},[137,63305,63306],{"class":157},"                    progressText.textContent ",[137,63308,253],{"class":143},[137,63310,63311],{"class":284}," `Loading images... ${",[137,63313,63314],{"class":157},"Math",[137,63316,1017],{"class":284},[137,63318,63319],{"class":147},"round",[137,63321,2774],{"class":284},[137,63323,63324],{"class":157},"loadedCount",[137,63326,38406],{"class":143},[137,63328,62999],{"class":157},[137,63330,1017],{"class":284},[137,63332,8611],{"class":364},[137,63334,219],{"class":284},[137,63336,7672],{"class":143},[137,63338,12500],{"class":364},[137,63340,14105],{"class":284},[137,63342,63343],{"class":284},"}%`",[137,63345,3276],{"class":157},[137,63347,63348,63351,63354,63356,63359,63361],{"class":139,"line":24827},[137,63349,63350],{"class":143},"                    if",[137,63352,63353],{"class":157}," (loadedCount ",[137,63355,5502],{"class":143},[137,63357,63358],{"class":157}," images.",[137,63360,8611],{"class":364},[137,63362,170],{"class":157},[137,63364,63365,63368,63370,63372,63375],{"class":139,"line":24857},[137,63366,63367],{"class":157},"                        loading.classList.",[137,63369,34393],{"class":147},[137,63371,356],{"class":157},[137,63373,63374],{"class":284},"\"hidden\"",[137,63376,1502],{"class":157},[137,63378,63379],{"class":139,"line":24862},[137,63380,63381],{"class":308},"                        \u002F\u002F Show center image initially\n",[137,63383,63384,63387,63389,63391,63393,63395],{"class":139,"line":24867},[137,63385,63386],{"class":147},"                        showClosestImage",[137,63388,356],{"class":157},[137,63390,6044],{"class":364},[137,63392,164],{"class":157},[137,63394,6044],{"class":364},[137,63396,1502],{"class":157},[137,63398,63399],{"class":139,"line":24884},[137,63400,63090],{"class":157},[137,63402,63403],{"class":139,"line":24892},[137,63404,36377],{"class":157},[137,63406,63407,63409,63411,63413,63415,63417],{"class":139,"line":24902},[137,63408,63281],{"class":157},[137,63410,50752],{"class":147},[137,63412,151],{"class":143},[137,63414,1484],{"class":157},[137,63416,222],{"class":143},[137,63418,256],{"class":157},[137,63420,63421,63423,63425],{"class":139,"line":24925},[137,63422,63297],{"class":157},[137,63424,12683],{"class":143},[137,63426,3276],{"class":157},[137,63428,63429],{"class":139,"line":24933},[137,63430,36377],{"class":157},[137,63432,63433],{"class":139,"line":24941},[137,63434,516],{"emptyLinePlaceholder":515},[137,63436,63437,63440,63442],{"class":139,"line":24952},[137,63438,63439],{"class":157},"                img.dataset.pitch ",[137,63441,253],{"class":143},[137,63443,63444],{"class":157}," imgInfo.pitch;\n",[137,63446,63447,63450,63452],{"class":139,"line":24960},[137,63448,63449],{"class":157},"                img.dataset.pupilX ",[137,63451,253],{"class":143},[137,63453,63454],{"class":157}," imgInfo.pupil_x;\n",[137,63456,63457,63460,63462],{"class":139,"line":24982},[137,63458,63459],{"class":157},"                img.dataset.pupilY ",[137,63461,253],{"class":143},[137,63463,63464],{"class":157}," imgInfo.pupil_y;\n",[137,63466,63467],{"class":139,"line":24989},[137,63468,516],{"emptyLinePlaceholder":515},[137,63470,63471,63474,63477],{"class":139,"line":24994},[137,63472,63473],{"class":157},"                container.",[137,63475,63476],{"class":147},"appendChild",[137,63478,63479],{"class":157},"(img);\n",[137,63481,63482,63485,63487],{"class":139,"line":24999},[137,63483,63484],{"class":157},"                imageElements.",[137,63486,35223],{"class":147},[137,63488,63489],{"class":157},"(imgInfo.filename, img);\n",[137,63491,63492],{"class":139,"line":25004},[137,63493,14336],{"class":157},[137,63495,63496],{"class":139,"line":25032},[137,63497,516],{"emptyLinePlaceholder":515},[137,63499,63500,63502,63505,63507,63509],{"class":139,"line":25040},[137,63501,63203],{"class":143},[137,63503,63504],{"class":157}," currentActive ",[137,63506,253],{"class":143},[137,63508,3417],{"class":364},[137,63510,3276],{"class":157},[137,63512,63513],{"class":139,"line":25052},[137,63514,516],{"emptyLinePlaceholder":515},[137,63516,63517,63519,63522,63524,63527],{"class":139,"line":25061},[137,63518,735],{"class":143},[137,63520,63521],{"class":147}," showImageByInfo",[137,63523,356],{"class":157},[137,63525,63526],{"class":161},"info",[137,63528,170],{"class":157},[137,63530,63531,63533,63535,63537],{"class":139,"line":25074},[137,63532,34330],{"class":143},[137,63534,61943],{"class":364},[137,63536,151],{"class":143},[137,63538,63539],{"class":157}," info.filename;\n",[137,63541,63542,63544,63546,63548,63551,63553],{"class":139,"line":25089},[137,63543,34330],{"class":143},[137,63545,63246],{"class":364},[137,63547,151],{"class":143},[137,63549,63550],{"class":157}," imageElements.",[137,63552,14153],{"class":147},[137,63554,63555],{"class":157},"(filename);\n",[137,63557,63558,63560,63562,63564,63567,63569],{"class":139,"line":25099},[137,63559,34351],{"class":143},[137,63561,158],{"class":157},[137,63563,17393],{"class":143},[137,63565,63566],{"class":157},"img) ",[137,63568,5428],{"class":143},[137,63570,3276],{"class":157},[137,63572,63573],{"class":139,"line":25108},[137,63574,516],{"emptyLinePlaceholder":515},[137,63576,63577,63579],{"class":139,"line":25118},[137,63578,34351],{"class":143},[137,63580,63581],{"class":157}," (currentActive) {\n",[137,63583,63584,63587,63589,63591,63593],{"class":139,"line":25123},[137,63585,63586],{"class":157},"                    currentActive.classList.",[137,63588,60131],{"class":147},[137,63590,356],{"class":157},[137,63592,60070],{"class":284},[137,63594,1502],{"class":157},[137,63596,63597],{"class":139,"line":25128},[137,63598,34372],{"class":157},[137,63600,63601,63604,63606,63608,63610],{"class":139,"line":25133},[137,63602,63603],{"class":157},"                img.classList.",[137,63605,34393],{"class":147},[137,63607,356],{"class":157},[137,63609,60070],{"class":284},[137,63611,1502],{"class":157},[137,63613,63614,63617,63619],{"class":139,"line":25138},[137,63615,63616],{"class":157},"                currentActive ",[137,63618,253],{"class":143},[137,63620,63621],{"class":157}," img;\n",[137,63623,63624],{"class":139,"line":25150},[137,63625,760],{"class":157},[137,63627,63628],{"class":139,"line":25162},[137,63629,516],{"emptyLinePlaceholder":515},[137,63631,63632],{"class":139,"line":25169},[137,63633,63634],{"class":308},"            \u002F\u002F Helper: map [-1, 1] to nearest value in array\n",[137,63636,63637,63639,63642,63644,63646,63648,63651],{"class":139,"line":25180},[137,63638,735],{"class":143},[137,63640,63641],{"class":147}," nearestInRange",[137,63643,356],{"class":157},[137,63645,5414],{"class":161},[137,63647,164],{"class":157},[137,63649,63650],{"class":161},"values",[137,63652,170],{"class":157},[137,63654,63655,63657,63660,63662,63665,63667],{"class":139,"line":25189},[137,63656,34330],{"class":143},[137,63658,63659],{"class":364}," min",[137,63661,151],{"class":143},[137,63663,63664],{"class":157}," values[",[137,63666,6044],{"class":364},[137,63668,5727],{"class":157},[137,63670,63671,63673,63676,63678,63681,63683,63686,63688],{"class":139,"line":25200},[137,63672,34330],{"class":143},[137,63674,63675],{"class":364}," max",[137,63677,151],{"class":143},[137,63679,63680],{"class":157}," values[values.",[137,63682,8611],{"class":364},[137,63684,63685],{"class":143}," -",[137,63687,8030],{"class":364},[137,63689,5727],{"class":157},[137,63691,63692,63694,63697,63699,63702,63704,63707,63709,63711,63713,63715,63717,63719,63721,63724,63726],{"class":139,"line":25213},[137,63693,34330],{"class":143},[137,63695,63696],{"class":364}," mapped",[137,63698,151],{"class":143},[137,63700,63701],{"class":157}," min ",[137,63703,182],{"class":143},[137,63705,63706],{"class":157}," ((value ",[137,63708,182],{"class":143},[137,63710,8030],{"class":364},[137,63712,219],{"class":157},[137,63714,47],{"class":143},[137,63716,40783],{"class":364},[137,63718,219],{"class":157},[137,63720,7672],{"class":143},[137,63722,63723],{"class":157}," (max ",[137,63725,8215],{"class":143},[137,63727,63728],{"class":157}," min);\n",[137,63730,63731,63734,63737,63739,63741,63743],{"class":139,"line":25222},[137,63732,63733],{"class":143},"                let",[137,63735,63736],{"class":157}," best ",[137,63738,253],{"class":143},[137,63740,63664],{"class":157},[137,63742,6044],{"class":364},[137,63744,5727],{"class":157},[137,63746,63747,63749,63752,63754,63757],{"class":139,"line":25231},[137,63748,63733],{"class":143},[137,63750,63751],{"class":157}," bestDist ",[137,63753,253],{"class":143},[137,63755,63756],{"class":364}," Infinity",[137,63758,3276],{"class":157},[137,63760,63761,63763,63765,63767,63770,63772],{"class":139,"line":25240},[137,63762,63022],{"class":143},[137,63764,158],{"class":157},[137,63766,3077],{"class":143},[137,63768,63769],{"class":364}," v",[137,63771,48913],{"class":143},[137,63773,63774],{"class":157}," values) {\n",[137,63776,63777,63780,63783,63785,63787,63790,63793,63795],{"class":139,"line":25245},[137,63778,63779],{"class":143},"                    const",[137,63781,63782],{"class":364}," d",[137,63784,151],{"class":143},[137,63786,7675],{"class":157},[137,63788,63789],{"class":147},"abs",[137,63791,63792],{"class":157},"(v ",[137,63794,8215],{"class":143},[137,63796,63797],{"class":157}," mapped);\n",[137,63799,63800,63802,63805,63807],{"class":139,"line":25250},[137,63801,63350],{"class":143},[137,63803,63804],{"class":157}," (d ",[137,63806,4033],{"class":143},[137,63808,63809],{"class":157}," bestDist) {\n",[137,63811,63813,63816,63818],{"class":139,"line":63812},91,[137,63814,63815],{"class":157},"                        bestDist ",[137,63817,253],{"class":143},[137,63819,63820],{"class":157}," d;\n",[137,63822,63824,63827,63829],{"class":139,"line":63823},92,[137,63825,63826],{"class":157},"                        best ",[137,63828,253],{"class":143},[137,63830,63831],{"class":157}," v;\n",[137,63833,63835],{"class":139,"line":63834},93,[137,63836,63090],{"class":157},[137,63838,63840],{"class":139,"line":63839},94,[137,63841,34372],{"class":157},[137,63843,63845,63847],{"class":139,"line":63844},95,[137,63846,5761],{"class":143},[137,63848,63849],{"class":157}," best;\n",[137,63851,63853],{"class":139,"line":63852},96,[137,63854,760],{"class":157},[137,63856,63858],{"class":139,"line":63857},97,[137,63859,516],{"emptyLinePlaceholder":515},[137,63861,63863,63865,63868,63870,63873,63875,63878],{"class":139,"line":63862},98,[137,63864,735],{"class":143},[137,63866,63867],{"class":147}," showClosestImage",[137,63869,356],{"class":157},[137,63871,63872],{"class":161},"normX",[137,63874,164],{"class":157},[137,63876,63877],{"class":161},"normY",[137,63879,170],{"class":157},[137,63881,63883],{"class":139,"line":63882},99,[137,63884,63885],{"class":308},"                \u002F\u002F normX, normY are from -1 to 1\n",[137,63887,63889,63891,63893,63895,63897],{"class":139,"line":63888},100,[137,63890,34330],{"class":143},[137,63892,63029],{"class":364},[137,63894,151],{"class":143},[137,63896,63641],{"class":147},[137,63898,63899],{"class":157},"(normX, pupilXValues);\n",[137,63901,63903,63905,63907,63909,63911,63913,63915,63918],{"class":139,"line":63902},101,[137,63904,34330],{"class":143},[137,63906,63045],{"class":364},[137,63908,151],{"class":143},[137,63910,63641],{"class":147},[137,63912,356],{"class":157},[137,63914,8215],{"class":143},[137,63916,63917],{"class":157},"normY, pupilYValues); ",[137,63919,63920],{"class":308},"\u002F\u002F invert so up = negative\n",[137,63922,63924,63926,63928,63930,63932,63934,63936],{"class":139,"line":63923},102,[137,63925,34330],{"class":143},[137,63927,61808],{"class":364},[137,63929,151],{"class":143},[137,63931,63641],{"class":147},[137,63933,356],{"class":157},[137,63935,8215],{"class":143},[137,63937,63938],{"class":157},"normY, pitchValues);\n",[137,63940,63942],{"class":139,"line":63941},103,[137,63943,516],{"emptyLinePlaceholder":515},[137,63945,63947,63949,63951,63953,63955,63957],{"class":139,"line":63946},104,[137,63948,34330],{"class":143},[137,63950,58075],{"class":364},[137,63952,151],{"class":143},[137,63954,63358],{"class":157},[137,63956,32346],{"class":147},[137,63958,11813],{"class":157},[137,63960,63962,63965,63967,63969,63971,63974,63976,63979,63981,63984,63986,63989,63991,63994,63996],{"class":139,"line":63961},105,[137,63963,63964],{"class":157},"                    (",[137,63966,63],{"class":161},[137,63968,219],{"class":157},[137,63970,222],{"class":143},[137,63972,63973],{"class":157}," img.pupil_x ",[137,63975,5502],{"class":143},[137,63977,63978],{"class":157}," pupil_x ",[137,63980,3351],{"class":143},[137,63982,63983],{"class":157}," img.pupil_y ",[137,63985,5502],{"class":143},[137,63987,63988],{"class":157}," pupil_y ",[137,63990,3351],{"class":143},[137,63992,63993],{"class":157}," img.pitch ",[137,63995,5502],{"class":143},[137,63997,63998],{"class":157}," pitch\n",[137,64000,64002],{"class":139,"line":64001},106,[137,64003,51842],{"class":157},[137,64005,64007,64009],{"class":139,"line":64006},107,[137,64008,34351],{"class":143},[137,64010,64011],{"class":157}," (match) {\n",[137,64013,64015,64018],{"class":139,"line":64014},108,[137,64016,64017],{"class":147},"                    showImageByInfo",[137,64019,64020],{"class":157},"(match);\n",[137,64022,64024],{"class":139,"line":64023},109,[137,64025,34372],{"class":157},[137,64027,64029],{"class":139,"line":64028},110,[137,64030,760],{"class":157},[137,64032,64034],{"class":139,"line":64033},111,[137,64035,516],{"emptyLinePlaceholder":515},[137,64037,64039],{"class":139,"line":64038},112,[137,64040,64041],{"class":308},"            \u002F\u002F Mouse tracking (desktop)\n",[137,64043,64045,64048,64050,64052,64055,64057,64059,64061,64063],{"class":139,"line":64044},113,[137,64046,64047],{"class":157},"            window.",[137,64049,4412],{"class":147},[137,64051,356],{"class":157},[137,64053,64054],{"class":284},"\"mousemove\"",[137,64056,24531],{"class":157},[137,64058,24534],{"class":161},[137,64060,219],{"class":157},[137,64062,222],{"class":143},[137,64064,256],{"class":157},[137,64066,64068,64070,64073,64075,64078,64081],{"class":139,"line":64067},114,[137,64069,34330],{"class":143},[137,64071,64072],{"class":364}," rect",[137,64074,151],{"class":143},[137,64076,64077],{"class":157}," container.",[137,64079,64080],{"class":147},"getBoundingClientRect",[137,64082,924],{"class":157},[137,64084,64086,64088,64090,64092,64095,64097,64100,64102,64104],{"class":139,"line":64085},115,[137,64087,34330],{"class":143},[137,64089,51205],{"class":364},[137,64091,151],{"class":143},[137,64093,64094],{"class":157}," rect.left ",[137,64096,182],{"class":143},[137,64098,64099],{"class":157}," rect.width ",[137,64101,47],{"class":143},[137,64103,40783],{"class":364},[137,64105,3276],{"class":157},[137,64107,64109,64111,64113,64115,64118,64120,64123,64125,64127],{"class":139,"line":64108},116,[137,64110,34330],{"class":143},[137,64112,51213],{"class":364},[137,64114,151],{"class":143},[137,64116,64117],{"class":157}," rect.top ",[137,64119,182],{"class":143},[137,64121,64122],{"class":157}," rect.height ",[137,64124,47],{"class":143},[137,64126,40783],{"class":364},[137,64128,3276],{"class":157},[137,64130,64132],{"class":139,"line":64131},117,[137,64133,516],{"emptyLinePlaceholder":515},[137,64135,64137],{"class":139,"line":64136},118,[137,64138,64139],{"class":308},"                \u002F\u002F -1..1 based on distance from center\n",[137,64141,64143,64145,64148,64150,64153,64155,64158,64160,64163,64165,64167],{"class":139,"line":64142},119,[137,64144,63733],{"class":143},[137,64146,64147],{"class":157}," normX ",[137,64149,253],{"class":143},[137,64151,64152],{"class":157}," (e.clientX ",[137,64154,8215],{"class":143},[137,64156,64157],{"class":157}," cx) ",[137,64159,47],{"class":143},[137,64161,64162],{"class":157}," (window.innerWidth ",[137,64164,47],{"class":143},[137,64166,40783],{"class":364},[137,64168,1502],{"class":157},[137,64170,64172,64174,64177,64179,64182,64184,64187,64189,64192,64194,64196],{"class":139,"line":64171},120,[137,64173,63733],{"class":143},[137,64175,64176],{"class":157}," normY ",[137,64178,253],{"class":143},[137,64180,64181],{"class":157}," (e.clientY ",[137,64183,8215],{"class":143},[137,64185,64186],{"class":157}," cy) ",[137,64188,47],{"class":143},[137,64190,64191],{"class":157}," (window.innerHeight ",[137,64193,47],{"class":143},[137,64195,40783],{"class":364},[137,64197,1502],{"class":157},[137,64199,64201],{"class":139,"line":64200},121,[137,64202,516],{"emptyLinePlaceholder":515},[137,64204,64206,64209,64211,64213,64216,64218,64220,64222,64225,64228,64230,64232],{"class":139,"line":64205},122,[137,64207,64208],{"class":157},"                normX ",[137,64210,253],{"class":143},[137,64212,7675],{"class":157},[137,64214,64215],{"class":147},"max",[137,64217,356],{"class":157},[137,64219,8215],{"class":143},[137,64221,6065],{"class":364},[137,64223,64224],{"class":157},", Math.",[137,64226,64227],{"class":147},"min",[137,64229,356],{"class":157},[137,64231,6065],{"class":364},[137,64233,64234],{"class":157},", normX));\n",[137,64236,64238,64241,64243,64245,64247,64249,64251,64253,64255,64257,64259,64261],{"class":139,"line":64237},123,[137,64239,64240],{"class":157},"                normY ",[137,64242,253],{"class":143},[137,64244,7675],{"class":157},[137,64246,64215],{"class":147},[137,64248,356],{"class":157},[137,64250,8215],{"class":143},[137,64252,6065],{"class":364},[137,64254,64224],{"class":157},[137,64256,64227],{"class":147},[137,64258,356],{"class":157},[137,64260,6065],{"class":364},[137,64262,64263],{"class":157},", normY));\n",[137,64265,64267],{"class":139,"line":64266},124,[137,64268,516],{"emptyLinePlaceholder":515},[137,64270,64272,64275],{"class":139,"line":64271},125,[137,64273,64274],{"class":147},"                showClosestImage",[137,64276,64277],{"class":157},"(normX, normY);\n",[137,64279,64281],{"class":139,"line":64280},126,[137,64282,14336],{"class":157},[137,64284,64286],{"class":139,"line":64285},127,[137,64287,516],{"emptyLinePlaceholder":515},[137,64289,64291],{"class":139,"line":64290},128,[137,64292,64293],{"class":308},"            \u002F\u002F Device motion (mobile)\n",[137,64295,64297,64299,64302],{"class":139,"line":64296},129,[137,64298,735],{"class":143},[137,64300,64301],{"class":147}," isMobile",[137,64303,275],{"class":157},[137,64305,64307,64309,64311,64313,64315,64317,64319,64321,64323,64326,64328,64330,64332,64334],{"class":139,"line":64306},130,[137,64308,5761],{"class":143},[137,64310,38406],{"class":284},[137,64312,53903],{"class":14746},[137,64314,7684],{"class":143},[137,64316,53908],{"class":14746},[137,64318,7684],{"class":143},[137,64320,53913],{"class":14746},[137,64322,7684],{"class":143},[137,64324,64325],{"class":14746},"iPod",[137,64327,47],{"class":284},[137,64329,53918],{"class":143},[137,64331,1017],{"class":157},[137,64333,53923],{"class":147},[137,64335,53926],{"class":157},[137,64337,64339],{"class":139,"line":64338},131,[137,64340,760],{"class":157},[137,64342,64344],{"class":139,"line":64343},132,[137,64345,516],{"emptyLinePlaceholder":515},[137,64347,64349,64352,64354,64357],{"class":139,"line":64348},133,[137,64350,64351],{"class":143},"            async",[137,64353,154],{"class":143},[137,64355,64356],{"class":147}," enableMotion",[137,64358,275],{"class":157},[137,64360,64362,64365],{"class":139,"line":64361},134,[137,64363,64364],{"class":143},"                try",[137,64366,256],{"class":157},[137,64368,64370,64372],{"class":139,"line":64369},135,[137,64371,63350],{"class":143},[137,64373,30009],{"class":157},[137,64375,64377,64380,64383,64385,64388],{"class":139,"line":64376},136,[137,64378,64379],{"class":143},"                        typeof",[137,64381,64382],{"class":157}," DeviceOrientationEvent ",[137,64384,26215],{"class":143},[137,64386,64387],{"class":284}," \"undefined\"",[137,64389,64390],{"class":143}," &&\n",[137,64392,64394,64396,64399,64401],{"class":139,"line":64393},137,[137,64395,64379],{"class":143},[137,64397,64398],{"class":157}," DeviceOrientationEvent.requestPermission ",[137,64400,5502],{"class":143},[137,64402,64403],{"class":284}," \"function\"\n",[137,64405,64407],{"class":139,"line":64406},138,[137,64408,64409],{"class":157},"                    ) {\n",[137,64411,64413,64415,64418,64420,64422,64425,64428],{"class":139,"line":64412},139,[137,64414,63054],{"class":143},[137,64416,64417],{"class":364}," perm",[137,64419,151],{"class":143},[137,64421,15069],{"class":143},[137,64423,64424],{"class":157}," DeviceOrientationEvent.",[137,64426,64427],{"class":147},"requestPermission",[137,64429,924],{"class":157},[137,64431,64433,64435,64438,64440,64443,64445,64447],{"class":139,"line":64432},140,[137,64434,36320],{"class":143},[137,64436,64437],{"class":157}," (perm ",[137,64439,26215],{"class":143},[137,64441,64442],{"class":284}," \"granted\"",[137,64444,219],{"class":157},[137,64446,5428],{"class":143},[137,64448,3276],{"class":157},[137,64450,64452],{"class":139,"line":64451},141,[137,64453,63090],{"class":157},[137,64455,64457],{"class":139,"line":64456},142,[137,64458,516],{"emptyLinePlaceholder":515},[137,64460,64462,64465,64467,64469,64472,64474,64476,64478,64480],{"class":139,"line":64461},143,[137,64463,64464],{"class":157},"                    window.",[137,64466,4412],{"class":147},[137,64468,356],{"class":157},[137,64470,64471],{"class":284},"\"deviceorientation\"",[137,64473,24531],{"class":157},[137,64475,24689],{"class":161},[137,64477,219],{"class":157},[137,64479,222],{"class":143},[137,64481,256],{"class":157},[137,64483,64485,64487,64490,64492,64495,64497,64499,64501],{"class":139,"line":64484},144,[137,64486,63054],{"class":143},[137,64488,64489],{"class":364}," beta",[137,64491,151],{"class":143},[137,64493,64494],{"class":157}," event.beta ",[137,64496,50706],{"class":143},[137,64498,7687],{"class":364},[137,64500,2323],{"class":157},[137,64502,64503],{"class":308},"\u002F\u002F front\u002Fback tilt (-180..180)\n",[137,64505,64507,64509,64512,64514,64517,64519,64521,64523],{"class":139,"line":64506},145,[137,64508,63054],{"class":143},[137,64510,64511],{"class":364}," gamma",[137,64513,151],{"class":143},[137,64515,64516],{"class":157}," event.gamma ",[137,64518,50706],{"class":143},[137,64520,7687],{"class":364},[137,64522,2323],{"class":157},[137,64524,64525],{"class":308},"\u002F\u002F left\u002Fright tilt (-90..90)\n",[137,64527,64529],{"class":139,"line":64528},146,[137,64530,516],{"emptyLinePlaceholder":515},[137,64532,64534],{"class":139,"line":64533},147,[137,64535,64536],{"class":308},"                        \u002F\u002F Map tilt to -1..1 range\n",[137,64538,64540,64542,64545,64547,64549,64551,64553,64555,64557,64559,64561,64563,64565,64568,64570,64573,64576],{"class":139,"line":64539},148,[137,64541,63054],{"class":143},[137,64543,64544],{"class":364}," normX",[137,64546,151],{"class":143},[137,64548,7675],{"class":157},[137,64550,64215],{"class":147},[137,64552,356],{"class":157},[137,64554,8215],{"class":143},[137,64556,6065],{"class":364},[137,64558,64224],{"class":157},[137,64560,64227],{"class":147},[137,64562,356],{"class":157},[137,64564,6065],{"class":364},[137,64566,64567],{"class":157},", gamma ",[137,64569,47],{"class":143},[137,64571,64572],{"class":364}," 45",[137,64574,64575],{"class":157},")); ",[137,64577,64578],{"class":308},"\u002F\u002F left\u002Fright\n",[137,64580,64582,64584,64587,64589,64591,64593,64595,64597,64599,64601,64603,64605,64607,64610,64612,64614,64616],{"class":139,"line":64581},149,[137,64583,63054],{"class":143},[137,64585,64586],{"class":364}," normY",[137,64588,151],{"class":143},[137,64590,7675],{"class":157},[137,64592,64215],{"class":147},[137,64594,356],{"class":157},[137,64596,8215],{"class":143},[137,64598,6065],{"class":364},[137,64600,64224],{"class":157},[137,64602,64227],{"class":147},[137,64604,356],{"class":157},[137,64606,6065],{"class":364},[137,64608,64609],{"class":157},", beta ",[137,64611,47],{"class":143},[137,64613,64572],{"class":364},[137,64615,64575],{"class":157},[137,64617,64618],{"class":308},"\u002F\u002F up\u002Fdown\n",[137,64620,64622],{"class":139,"line":64621},150,[137,64623,516],{"emptyLinePlaceholder":515},[137,64625,64627,64629],{"class":139,"line":64626},151,[137,64628,63386],{"class":147},[137,64630,64277],{"class":157},[137,64632,64634],{"class":139,"line":64633},152,[137,64635,64636],{"class":157},"                    });\n",[137,64638,64640],{"class":139,"line":64639},153,[137,64641,516],{"emptyLinePlaceholder":515},[137,64643,64645,64648,64650,64652,64654],{"class":139,"line":64644},154,[137,64646,64647],{"class":157},"                    motionButton.classList.",[137,64649,34393],{"class":147},[137,64651,356],{"class":157},[137,64653,63374],{"class":284},[137,64655,1502],{"class":157},[137,64657,64659,64662,64664],{"class":139,"line":64658},155,[137,64660,64661],{"class":157},"                } ",[137,64663,2807],{"class":143},[137,64665,62559],{"class":157},[137,64667,64669,64672,64674],{"class":139,"line":64668},156,[137,64670,64671],{"class":157},"                    console.",[137,64673,2812],{"class":147},[137,64675,64676],{"class":157},"(e);\n",[137,64678,64680],{"class":139,"line":64679},157,[137,64681,34372],{"class":157},[137,64683,64685],{"class":139,"line":64684},158,[137,64686,760],{"class":157},[137,64688,64690],{"class":139,"line":64689},159,[137,64691,516],{"emptyLinePlaceholder":515},[137,64693,64695,64697,64699,64702],{"class":139,"line":64694},160,[137,64696,5747],{"class":143},[137,64698,158],{"class":157},[137,64700,64701],{"class":147},"isMobile",[137,64703,64704],{"class":157},"()) {\n",[137,64706,64708,64711,64713,64715,64717],{"class":139,"line":64707},161,[137,64709,64710],{"class":157},"                motionButton.classList.",[137,64712,60131],{"class":147},[137,64714,356],{"class":157},[137,64716,63374],{"class":284},[137,64718,1502],{"class":157},[137,64720,64722,64725,64727,64729,64731],{"class":139,"line":64721},162,[137,64723,64724],{"class":157},"                motionButton.",[137,64726,4412],{"class":147},[137,64728,356],{"class":157},[137,64730,26258],{"class":284},[137,64732,64733],{"class":157},", enableMotion);\n",[137,64735,64737,64739,64741],{"class":139,"line":64736},163,[137,64738,50677],{"class":157},[137,64740,24947],{"class":143},[137,64742,256],{"class":157},[137,64744,64746,64748,64750,64752,64754],{"class":139,"line":64745},164,[137,64747,64710],{"class":157},[137,64749,34393],{"class":147},[137,64751,356],{"class":157},[137,64753,63374],{"class":284},[137,64755,1502],{"class":157},[137,64757,64759],{"class":139,"line":64758},165,[137,64760,760],{"class":157},[137,64762,64764,64766,64768],{"class":139,"line":64763},166,[137,64765,9843],{"class":157},[137,64767,4037],{"class":4036},[137,64769,4053],{"class":157},[137,64771,64773,64775,64777],{"class":139,"line":64772},167,[137,64774,8374],{"class":157},[137,64776,4065],{"class":4036},[137,64778,4053],{"class":157},[137,64780,64782,64784,64786],{"class":139,"line":64781},168,[137,64783,4083],{"class":157},[137,64785,4026],{"class":4036},[137,64787,4053],{"class":157},[27,64789,64790],{},"I've excluded the CSS styling in the example above since the code snippet is already long, but feel free to look at the GitHub project attached at the end of the post to see how I styled it. The important part of the snippet above is the JavaScript at the bottom, where we're setting up our data structures. We're recreating the same list of image combinations we generated earlier, and we're initialising a Map to store references to the actual image elements once we create them.",[104,64792,64794],{"id":64793},"preloading-all-the-images-and-displaying-only-one-at-a-time","Preloading All the Images and Displaying Only One at a Time",[27,64796,64797,64798,61434],{},"The next crucial step is to preload all 45 images into the browser's memory. We want to do this upfront so that when the user moves their mouse, we can instantly swap between images without any loading delay or flicker. Let's add functions to handle this. Add this code to your ",[22,64799,4295],{},[128,64801,64803],{"className":4024,"code":64802,"language":4026,"meta":133,"style":133},"\u003Cscript>\n    \u002F\u002F ... (previous code) ...\n\n    function preloadAllImages() {\n        return new Promise((resolve) => {\n            let loaded = 0;\n            const total = images.length;\n\n            images.forEach((info) => {\n                const img = new Image();\n                img.src = `.\u002Fgenerated-images\u002F${info.filename}`;\n                img.style.position = \"absolute\";\n                img.style.top = \"0\";\n                img.style.left = \"0\";\n                img.style.width = \"100%\";\n                img.style.height = \"100%\";\n                img.style.objectFit = \"contain\";\n                img.style.display = \"none\"; \u002F\u002F hidden by default\n\n                img.onload = () => {\n                    loaded++;\n                    if (loaded === total) resolve();\n                };\n\n                const key = `${info.pitch}_${info.pupil_x}_${info.pupil_y}`;\n                imageElements.set(key, img);\n                container.appendChild(img);\n            });\n        });\n    }\n\n    function showImage(key) {\n        if (currentKey && imageElements.has(currentKey)) {\n            imageElements.get(currentKey).style.display = \"none\";\n        }\n        if (imageElements.has(key)) {\n            imageElements.get(key).style.display = \"block\";\n            currentKey = key;\n        }\n    }\n\u003C\u002Fscript>\n",[22,64804,64805,64813,64818,64822,64831,64849,64862,64877,64881,64897,64911,64929,64941,64953,64964,64976,64987,64999,65014,65018,65032,65041,65057,65061,65065,65103,65112,65120,65124,65128,65132,65136,65149,65165,65181,65185,65197,65213,65223,65227,65231],{"__ignoreMap":133},[137,64806,64807,64809,64811],{"class":139,"line":140},[137,64808,4033],{"class":157},[137,64810,4037],{"class":4036},[137,64812,4053],{"class":157},[137,64814,64815],{"class":139,"line":173},[137,64816,64817],{"class":308},"    \u002F\u002F ... (previous code) ...\n",[137,64819,64820],{"class":139,"line":188},[137,64821,516],{"emptyLinePlaceholder":515},[137,64823,64824,64826,64829],{"class":139,"line":269},[137,64825,3398],{"class":143},[137,64827,64828],{"class":147}," preloadAllImages",[137,64830,275],{"class":157},[137,64832,64833,64835,64837,64839,64841,64843,64845,64847],{"class":139,"line":278},[137,64834,5472],{"class":143},[137,64836,1426],{"class":143},[137,64838,14116],{"class":364},[137,64840,2774],{"class":157},[137,64842,48591],{"class":161},[137,64844,219],{"class":157},[137,64846,222],{"class":143},[137,64848,256],{"class":157},[137,64850,64851,64853,64856,64858,64860],{"class":139,"line":291},[137,64852,63203],{"class":143},[137,64854,64855],{"class":157}," loaded ",[137,64857,253],{"class":143},[137,64859,7687],{"class":364},[137,64861,3276],{"class":157},[137,64863,64864,64866,64869,64871,64873,64875],{"class":139,"line":297},[137,64865,5772],{"class":143},[137,64867,64868],{"class":364}," total",[137,64870,151],{"class":143},[137,64872,63358],{"class":157},[137,64874,8611],{"class":364},[137,64876,3276],{"class":157},[137,64878,64879],{"class":139,"line":302},[137,64880,516],{"emptyLinePlaceholder":515},[137,64882,64883,64885,64887,64889,64891,64893,64895],{"class":139,"line":662},[137,64884,63226],{"class":157},[137,64886,8564],{"class":147},[137,64888,2774],{"class":157},[137,64890,63526],{"class":161},[137,64892,219],{"class":157},[137,64894,222],{"class":143},[137,64896,256],{"class":157},[137,64898,64899,64901,64903,64905,64907,64909],{"class":139,"line":667},[137,64900,34330],{"class":143},[137,64902,63246],{"class":364},[137,64904,151],{"class":143},[137,64906,1426],{"class":143},[137,64908,63253],{"class":147},[137,64910,924],{"class":157},[137,64912,64913,64915,64917,64919,64921,64923,64925,64927],{"class":139,"line":786},[137,64914,63260],{"class":157},[137,64916,253],{"class":143},[137,64918,63265],{"class":284},[137,64920,63526],{"class":157},[137,64922,1017],{"class":284},[137,64924,63272],{"class":157},[137,64926,4706],{"class":284},[137,64928,3276],{"class":157},[137,64930,64931,64934,64936,64939],{"class":139,"line":798},[137,64932,64933],{"class":157},"                img.style.position ",[137,64935,253],{"class":143},[137,64937,64938],{"class":284}," \"absolute\"",[137,64940,3276],{"class":157},[137,64942,64943,64946,64948,64951],{"class":139,"line":803},[137,64944,64945],{"class":157},"                img.style.top ",[137,64947,253],{"class":143},[137,64949,64950],{"class":284}," \"0\"",[137,64952,3276],{"class":157},[137,64954,64955,64958,64960,64962],{"class":139,"line":931},[137,64956,64957],{"class":157},"                img.style.left ",[137,64959,253],{"class":143},[137,64961,64950],{"class":284},[137,64963,3276],{"class":157},[137,64965,64966,64969,64971,64974],{"class":139,"line":1568},[137,64967,64968],{"class":157},"                img.style.width ",[137,64970,253],{"class":143},[137,64972,64973],{"class":284}," \"100%\"",[137,64975,3276],{"class":157},[137,64977,64978,64981,64983,64985],{"class":139,"line":1573},[137,64979,64980],{"class":157},"                img.style.height ",[137,64982,253],{"class":143},[137,64984,64973],{"class":284},[137,64986,3276],{"class":157},[137,64988,64989,64992,64994,64997],{"class":139,"line":1578},[137,64990,64991],{"class":157},"                img.style.objectFit ",[137,64993,253],{"class":143},[137,64995,64996],{"class":284}," \"contain\"",[137,64998,3276],{"class":157},[137,65000,65001,65004,65006,65009,65011],{"class":139,"line":1588},[137,65002,65003],{"class":157},"                img.style.display ",[137,65005,253],{"class":143},[137,65007,65008],{"class":284}," \"none\"",[137,65010,2323],{"class":157},[137,65012,65013],{"class":308},"\u002F\u002F hidden by default\n",[137,65015,65016],{"class":139,"line":1601},[137,65017,516],{"emptyLinePlaceholder":515},[137,65019,65020,65022,65024,65026,65028,65030],{"class":139,"line":3802},[137,65021,63281],{"class":157},[137,65023,63284],{"class":147},[137,65025,151],{"class":143},[137,65027,1484],{"class":157},[137,65029,222],{"class":143},[137,65031,256],{"class":157},[137,65033,65034,65037,65039],{"class":139,"line":3808},[137,65035,65036],{"class":157},"                    loaded",[137,65038,12683],{"class":143},[137,65040,3276],{"class":157},[137,65042,65043,65045,65048,65050,65053,65055],{"class":139,"line":3822},[137,65044,63350],{"class":143},[137,65046,65047],{"class":157}," (loaded ",[137,65049,5502],{"class":143},[137,65051,65052],{"class":157}," total) ",[137,65054,48591],{"class":147},[137,65056,924],{"class":157},[137,65058,65059],{"class":139,"line":3827},[137,65060,36377],{"class":157},[137,65062,65063],{"class":139,"line":3832},[137,65064,516],{"emptyLinePlaceholder":515},[137,65066,65067,65069,65072,65074,65076,65078,65080,65082,65085,65087,65089,65091,65093,65095,65097,65099,65101],{"class":139,"line":3840},[137,65068,34330],{"class":143},[137,65070,65071],{"class":364}," key",[137,65073,151],{"class":143},[137,65075,4686],{"class":284},[137,65077,63526],{"class":157},[137,65079,1017],{"class":284},[137,65081,63063],{"class":157},[137,65083,65084],{"class":284},"}_${",[137,65086,63526],{"class":157},[137,65088,1017],{"class":284},[137,65090,61285],{"class":157},[137,65092,65084],{"class":284},[137,65094,63526],{"class":157},[137,65096,1017],{"class":284},[137,65098,61295],{"class":157},[137,65100,4706],{"class":284},[137,65102,3276],{"class":157},[137,65104,65105,65107,65109],{"class":139,"line":3846},[137,65106,63484],{"class":157},[137,65108,35223],{"class":147},[137,65110,65111],{"class":157},"(key, img);\n",[137,65113,65114,65116,65118],{"class":139,"line":3861},[137,65115,63473],{"class":157},[137,65117,63476],{"class":147},[137,65119,63479],{"class":157},[137,65121,65122],{"class":139,"line":3883},[137,65123,14336],{"class":157},[137,65125,65126],{"class":139,"line":3896},[137,65127,14079],{"class":157},[137,65129,65130],{"class":139,"line":3901},[137,65131,294],{"class":157},[137,65133,65134],{"class":139,"line":3906},[137,65135,516],{"emptyLinePlaceholder":515},[137,65137,65138,65140,65143,65145,65147],{"class":139,"line":3911},[137,65139,3398],{"class":143},[137,65141,65142],{"class":147}," showImage",[137,65144,356],{"class":157},[137,65146,12632],{"class":161},[137,65148,170],{"class":157},[137,65150,65151,65153,65156,65158,65160,65162],{"class":139,"line":4666},[137,65152,5496],{"class":143},[137,65154,65155],{"class":157}," (currentKey ",[137,65157,3351],{"class":143},[137,65159,63550],{"class":157},[137,65161,34272],{"class":147},[137,65163,65164],{"class":157},"(currentKey)) {\n",[137,65166,65167,65170,65172,65175,65177,65179],{"class":139,"line":4672},[137,65168,65169],{"class":157},"            imageElements.",[137,65171,14153],{"class":147},[137,65173,65174],{"class":157},"(currentKey).style.display ",[137,65176,253],{"class":143},[137,65178,65008],{"class":284},[137,65180,3276],{"class":157},[137,65182,65183],{"class":139,"line":4680},[137,65184,1966],{"class":157},[137,65186,65187,65189,65192,65194],{"class":139,"line":4711},[137,65188,5496],{"class":143},[137,65190,65191],{"class":157}," (imageElements.",[137,65193,34272],{"class":147},[137,65195,65196],{"class":157},"(key)) {\n",[137,65198,65199,65201,65203,65206,65208,65211],{"class":139,"line":4716},[137,65200,65169],{"class":157},[137,65202,14153],{"class":147},[137,65204,65205],{"class":157},"(key).style.display ",[137,65207,253],{"class":143},[137,65209,65210],{"class":284}," \"block\"",[137,65212,3276],{"class":157},[137,65214,65215,65218,65220],{"class":139,"line":4721},[137,65216,65217],{"class":157},"            currentKey ",[137,65219,253],{"class":143},[137,65221,65222],{"class":157}," key;\n",[137,65224,65225],{"class":139,"line":4727},[137,65226,1966],{"class":157},[137,65228,65229],{"class":139,"line":4732},[137,65230,294],{"class":157},[137,65232,65233,65235,65237],{"class":139,"line":5006},[137,65234,4083],{"class":157},[137,65236,4037],{"class":4036},[137,65238,4053],{"class":157},[27,65240,4737,65241,65244,65245,65247],{},[22,65242,65243],{},"preloadAllImages()"," function creates a new Image element for each of our 45 generated files. It sets the source of each image to point to the corresponding file in our ",[22,65246,62394],{}," folder. Crucially, it positions all these images absolutely at the exact same location (top-left corner of the container) so they'll stack perfectly on top of each other.",[27,65249,65250,65251,65254],{},"The function returns a ",[22,65252,65253],{},"Promise"," that resolves once all images have finished loading. This is important because we don't want to start showing images to the user before they're all ready -that would result in broken image icons or loading spinners.",[27,65256,4737,65257,65260,65261,65264,65265,65268],{},[22,65258,65259],{},"showImage()"," function is simple - it hides whatever image is currently visible and shows the image corresponding to the key we pass in. The key is a string like ",[22,65262,65263],{},"\"0_0_0\""," (for the neutral position) or ",[22,65266,65267],{},"\"-10_15_0\""," (for head tilted slightly down, eyes looking right, vertical eye position neutral).",[27,65270,65271],{},"Now let's add an initialisation function that preloads everything and shows the neutral face to start with:",[128,65273,65275],{"className":4024,"code":65274,"language":4026,"meta":133,"style":133},"\u003Cscript>\n    \u002F\u002F ... (previous code) ...\n\n    async function init() {\n        await preloadAllImages();\n        showImage(\"0_0_0\"); \u002F\u002F pitch=0, pupil_x=0, pupil_y=0\n    }\n\n    init();\n\u003C\u002Fscript>\n",[22,65276,65277,65285,65289,65293,65303,65311,65325,65329,65333,65340],{"__ignoreMap":133},[137,65278,65279,65281,65283],{"class":139,"line":140},[137,65280,4033],{"class":157},[137,65282,4037],{"class":4036},[137,65284,4053],{"class":157},[137,65286,65287],{"class":139,"line":173},[137,65288,64817],{"class":308},[137,65290,65291],{"class":139,"line":188},[137,65292,516],{"emptyLinePlaceholder":515},[137,65294,65295,65297,65299,65301],{"class":139,"line":269},[137,65296,15546],{"class":143},[137,65298,154],{"class":143},[137,65300,9539],{"class":147},[137,65302,275],{"class":157},[137,65304,65305,65307,65309],{"class":139,"line":278},[137,65306,25043],{"class":143},[137,65308,64828],{"class":147},[137,65310,924],{"class":157},[137,65312,65313,65316,65318,65320,65322],{"class":139,"line":291},[137,65314,65315],{"class":147},"        showImage",[137,65317,356],{"class":157},[137,65319,65263],{"class":284},[137,65321,1839],{"class":157},[137,65323,65324],{"class":308},"\u002F\u002F pitch=0, pupil_x=0, pupil_y=0\n",[137,65326,65327],{"class":139,"line":297},[137,65328,294],{"class":157},[137,65330,65331],{"class":139,"line":302},[137,65332,516],{"emptyLinePlaceholder":515},[137,65334,65335,65338],{"class":139,"line":662},[137,65336,65337],{"class":147},"    init",[137,65339,924],{"class":157},[137,65341,65342,65344,65346],{"class":139,"line":667},[137,65343,4083],{"class":157},[137,65345,4037],{"class":4036},[137,65347,4053],{"class":157},[27,65349,65350],{},"With this code in place, when you open your HTML file in a browser, it will load all 45 images and then display the neutral face (looking straight ahead). But it won't follow your mouse ye -that's what we'll implement next.",[104,65352,65354],{"id":65353},"mapping-mouse-position-to-the-correct-image","Mapping Mouse Position to the Correct Image",[27,65356,65357,65358,164,65360,164,65362,65364],{},"Here's where the magic really happens. We need to convert the user's mouse position into our three-dimensional parameter space (",[22,65359,63063],{},[22,65361,61285],{},[22,65363,61295],{},") and then find the closest image in our library that matches those parameters. Let's break this down into manageable steps:",[2569,65366,65367,65379,65382],{},[1006,65368,65369,65370,10928,65373,65375,65376,65378],{},"First, we'll convert the mouse's X and Y coordinates into a normalised range from ",[22,65371,65372],{},"-1",[22,65374,6065],{},", where ",[22,65377,6044],{}," represents the center of the screen.",[1006,65380,65381],{},"Then we'll use those normalised values to calculate target values for our three parameters.",[1006,65383,65384],{},"Finally, we'll \"snap\" to the nearest values that we actually have in our generated image set.",[27,65386,65387],{},"Add this code to your script section:",[128,65389,65391],{"className":4024,"code":65390,"language":4026,"meta":133,"style":133},"\u003Cscript>\n    \u002F\u002F ... (previous code) ...\n\n    function nearest(value, allowed) {\n        return allowed.reduce((prev, curr) => (Math.abs(curr - value) \u003C Math.abs(prev - value) ? curr : prev));\n    }\n\n    function updateFromMouse(mouseX, mouseY) {\n        const centerX = window.innerWidth \u002F 2;\n        const centerY = window.innerHeight \u002F 2;\n\n        \u002F\u002F -1..1\n        let offsetX = (mouseX - centerX) \u002F centerX;\n        let offsetY = (mouseY - centerY) \u002F centerY;\n\n        offsetX = Math.max(-1, Math.min(1, offsetX));\n        offsetY = Math.max(-1, Math.min(1, offsetY));\n\n        const targetPupilX = offsetX * 15; \u002F\u002F full range [-15, 15]\n        const targetPupilY = -offsetY * 15; \u002F\u002F invert so up = negative\n        const targetPitch = offsetY * 20; \u002F\u002F [-20, 20]\n\n        const snapPitch = nearest(targetPitch, pitchValues);\n        const snapPupilX = nearest(targetPupilX, pupilXValues);\n        const snapPupilY = nearest(targetPupilY, pupilYValues);\n\n        const key = `${snapPitch}_${snapPupilX}_${snapPupilY}`;\n        showImage(key);\n    }\n\n    window.addEventListener(\"mousemove\", (e) => {\n        updateFromMouse(e.clientX, e.clientY);\n    });\n\u003C\u002Fscript>\n",[22,65392,65393,65401,65405,65409,65427,65485,65489,65493,65512,65530,65548,65552,65557,65580,65602,65606,65634,65662,65666,65687,65709,65729,65733,65747,65761,65775,65779,65806,65813,65817,65821,65842,65850,65854],{"__ignoreMap":133},[137,65394,65395,65397,65399],{"class":139,"line":140},[137,65396,4033],{"class":157},[137,65398,4037],{"class":4036},[137,65400,4053],{"class":157},[137,65402,65403],{"class":139,"line":173},[137,65404,64817],{"class":308},[137,65406,65407],{"class":139,"line":188},[137,65408,516],{"emptyLinePlaceholder":515},[137,65410,65411,65413,65416,65418,65420,65422,65425],{"class":139,"line":269},[137,65412,3398],{"class":143},[137,65414,65415],{"class":147}," nearest",[137,65417,356],{"class":157},[137,65419,5414],{"class":161},[137,65421,164],{"class":157},[137,65423,65424],{"class":161},"allowed",[137,65426,170],{"class":157},[137,65428,65429,65431,65434,65437,65439,65441,65443,65446,65448,65450,65452,65454,65457,65459,65462,65464,65466,65468,65471,65473,65475,65477,65480,65482],{"class":139,"line":278},[137,65430,5472],{"class":143},[137,65432,65433],{"class":157}," allowed.",[137,65435,65436],{"class":147},"reduce",[137,65438,2774],{"class":157},[137,65440,50691],{"class":161},[137,65442,164],{"class":157},[137,65444,65445],{"class":161},"curr",[137,65447,219],{"class":157},[137,65449,222],{"class":143},[137,65451,33954],{"class":157},[137,65453,63789],{"class":147},[137,65455,65456],{"class":157},"(curr ",[137,65458,8215],{"class":143},[137,65460,65461],{"class":157}," value) ",[137,65463,4033],{"class":143},[137,65465,7675],{"class":157},[137,65467,63789],{"class":147},[137,65469,65470],{"class":157},"(prev ",[137,65472,8215],{"class":143},[137,65474,65461],{"class":157},[137,65476,12972],{"class":143},[137,65478,65479],{"class":157}," curr ",[137,65481,894],{"class":143},[137,65483,65484],{"class":157}," prev));\n",[137,65486,65487],{"class":139,"line":291},[137,65488,294],{"class":157},[137,65490,65491],{"class":139,"line":297},[137,65492,516],{"emptyLinePlaceholder":515},[137,65494,65495,65497,65500,65502,65505,65507,65510],{"class":139,"line":302},[137,65496,3398],{"class":143},[137,65498,65499],{"class":147}," updateFromMouse",[137,65501,356],{"class":157},[137,65503,65504],{"class":161},"mouseX",[137,65506,164],{"class":157},[137,65508,65509],{"class":161},"mouseY",[137,65511,170],{"class":157},[137,65513,65514,65516,65519,65521,65524,65526,65528],{"class":139,"line":662},[137,65515,3008],{"class":143},[137,65517,65518],{"class":364}," centerX",[137,65520,151],{"class":143},[137,65522,65523],{"class":157}," window.innerWidth ",[137,65525,47],{"class":143},[137,65527,40783],{"class":364},[137,65529,3276],{"class":157},[137,65531,65532,65534,65537,65539,65542,65544,65546],{"class":139,"line":667},[137,65533,3008],{"class":143},[137,65535,65536],{"class":364}," centerY",[137,65538,151],{"class":143},[137,65540,65541],{"class":157}," window.innerHeight ",[137,65543,47],{"class":143},[137,65545,40783],{"class":364},[137,65547,3276],{"class":157},[137,65549,65550],{"class":139,"line":786},[137,65551,516],{"emptyLinePlaceholder":515},[137,65553,65554],{"class":139,"line":798},[137,65555,65556],{"class":308},"        \u002F\u002F -1..1\n",[137,65558,65559,65562,65565,65567,65570,65572,65575,65577],{"class":139,"line":803},[137,65560,65561],{"class":143},"        let",[137,65563,65564],{"class":157}," offsetX ",[137,65566,253],{"class":143},[137,65568,65569],{"class":157}," (mouseX ",[137,65571,8215],{"class":143},[137,65573,65574],{"class":157}," centerX) ",[137,65576,47],{"class":143},[137,65578,65579],{"class":157}," centerX;\n",[137,65581,65582,65584,65587,65589,65592,65594,65597,65599],{"class":139,"line":931},[137,65583,65561],{"class":143},[137,65585,65586],{"class":157}," offsetY ",[137,65588,253],{"class":143},[137,65590,65591],{"class":157}," (mouseY ",[137,65593,8215],{"class":143},[137,65595,65596],{"class":157}," centerY) ",[137,65598,47],{"class":143},[137,65600,65601],{"class":157}," centerY;\n",[137,65603,65604],{"class":139,"line":1568},[137,65605,516],{"emptyLinePlaceholder":515},[137,65607,65608,65611,65613,65615,65617,65619,65621,65623,65625,65627,65629,65631],{"class":139,"line":1573},[137,65609,65610],{"class":157},"        offsetX ",[137,65612,253],{"class":143},[137,65614,7675],{"class":157},[137,65616,64215],{"class":147},[137,65618,356],{"class":157},[137,65620,8215],{"class":143},[137,65622,6065],{"class":364},[137,65624,64224],{"class":157},[137,65626,64227],{"class":147},[137,65628,356],{"class":157},[137,65630,6065],{"class":364},[137,65632,65633],{"class":157},", offsetX));\n",[137,65635,65636,65639,65641,65643,65645,65647,65649,65651,65653,65655,65657,65659],{"class":139,"line":1578},[137,65637,65638],{"class":157},"        offsetY ",[137,65640,253],{"class":143},[137,65642,7675],{"class":157},[137,65644,64215],{"class":147},[137,65646,356],{"class":157},[137,65648,8215],{"class":143},[137,65650,6065],{"class":364},[137,65652,64224],{"class":157},[137,65654,64227],{"class":147},[137,65656,356],{"class":157},[137,65658,6065],{"class":364},[137,65660,65661],{"class":157},", offsetY));\n",[137,65663,65664],{"class":139,"line":1588},[137,65665,516],{"emptyLinePlaceholder":515},[137,65667,65668,65670,65673,65675,65677,65679,65682,65684],{"class":139,"line":1601},[137,65669,3008],{"class":143},[137,65671,65672],{"class":364}," targetPupilX",[137,65674,151],{"class":143},[137,65676,65564],{"class":157},[137,65678,7672],{"class":143},[137,65680,65681],{"class":364}," 15",[137,65683,2323],{"class":157},[137,65685,65686],{"class":308},"\u002F\u002F full range [-15, 15]\n",[137,65688,65689,65691,65694,65696,65698,65701,65703,65705,65707],{"class":139,"line":3802},[137,65690,3008],{"class":143},[137,65692,65693],{"class":364}," targetPupilY",[137,65695,151],{"class":143},[137,65697,63685],{"class":143},[137,65699,65700],{"class":157},"offsetY ",[137,65702,7672],{"class":143},[137,65704,65681],{"class":364},[137,65706,2323],{"class":157},[137,65708,63920],{"class":308},[137,65710,65711,65713,65716,65718,65720,65722,65724,65726],{"class":139,"line":3808},[137,65712,3008],{"class":143},[137,65714,65715],{"class":364}," targetPitch",[137,65717,151],{"class":143},[137,65719,65586],{"class":157},[137,65721,7672],{"class":143},[137,65723,60522],{"class":364},[137,65725,2323],{"class":157},[137,65727,65728],{"class":308},"\u002F\u002F [-20, 20]\n",[137,65730,65731],{"class":139,"line":3822},[137,65732,516],{"emptyLinePlaceholder":515},[137,65734,65735,65737,65740,65742,65744],{"class":139,"line":3827},[137,65736,3008],{"class":143},[137,65738,65739],{"class":364}," snapPitch",[137,65741,151],{"class":143},[137,65743,65415],{"class":147},[137,65745,65746],{"class":157},"(targetPitch, pitchValues);\n",[137,65748,65749,65751,65754,65756,65758],{"class":139,"line":3832},[137,65750,3008],{"class":143},[137,65752,65753],{"class":364}," snapPupilX",[137,65755,151],{"class":143},[137,65757,65415],{"class":147},[137,65759,65760],{"class":157},"(targetPupilX, pupilXValues);\n",[137,65762,65763,65765,65768,65770,65772],{"class":139,"line":3840},[137,65764,3008],{"class":143},[137,65766,65767],{"class":364}," snapPupilY",[137,65769,151],{"class":143},[137,65771,65415],{"class":147},[137,65773,65774],{"class":157},"(targetPupilY, pupilYValues);\n",[137,65776,65777],{"class":139,"line":3846},[137,65778,516],{"emptyLinePlaceholder":515},[137,65780,65781,65783,65785,65787,65789,65792,65794,65797,65799,65802,65804],{"class":139,"line":3861},[137,65782,3008],{"class":143},[137,65784,65071],{"class":364},[137,65786,151],{"class":143},[137,65788,4686],{"class":284},[137,65790,65791],{"class":157},"snapPitch",[137,65793,65084],{"class":284},[137,65795,65796],{"class":157},"snapPupilX",[137,65798,65084],{"class":284},[137,65800,65801],{"class":157},"snapPupilY",[137,65803,4706],{"class":284},[137,65805,3276],{"class":157},[137,65807,65808,65810],{"class":139,"line":3883},[137,65809,65315],{"class":147},[137,65811,65812],{"class":157},"(key);\n",[137,65814,65815],{"class":139,"line":3896},[137,65816,294],{"class":157},[137,65818,65819],{"class":139,"line":3901},[137,65820,516],{"emptyLinePlaceholder":515},[137,65822,65823,65826,65828,65830,65832,65834,65836,65838,65840],{"class":139,"line":3906},[137,65824,65825],{"class":157},"    window.",[137,65827,4412],{"class":147},[137,65829,356],{"class":157},[137,65831,64054],{"class":284},[137,65833,24531],{"class":157},[137,65835,24534],{"class":161},[137,65837,219],{"class":157},[137,65839,222],{"class":143},[137,65841,256],{"class":157},[137,65843,65844,65847],{"class":139,"line":3911},[137,65845,65846],{"class":147},"        updateFromMouse",[137,65848,65849],{"class":157},"(e.clientX, e.clientY);\n",[137,65851,65852],{"class":139,"line":4666},[137,65853,2832],{"class":157},[137,65855,65856,65858,65860],{"class":139,"line":4672},[137,65857,4083],{"class":157},[137,65859,4037],{"class":4036},[137,65861,4053],{"class":157},[27,65863,65864],{},"When the user moves their mouse, we receive the absolute X and Y coordinates of the cursor. But what we really need to know is: \"how far is the cursor from the center of the screen, and in which direction?\"",[27,65866,65867,65868,114,65871,65874],{},"So we calculate ",[22,65869,65870],{},"offsetX",[22,65872,65873],{},"offsetY",", which represent the mouse position relative to the screen center, normalised so that -1 means \"all the way to the left\u002Ftop\" and 1 means \"all the way to the right\u002Fbottom\". The center of the screen is 0 for both.",[27,65876,65877,65878,65880,65881,65883],{},"Then we multiply these normalised offsets by our parameter ranges. For horizontal eye movement (",[22,65879,61285],{},"), we simply multiply by 15. For vertical eye movement (",[22,65882,61295],{},"), we multiply by 15 but also negate it, because in screen coordinates, positive Y goes downward, but in our face model, we want positive values to look upward. For head pitch, we multiply by 20 to get the full range of head tilts.",[27,65885,65886,65887,65890,65891,65893,65894,65897,65898,65901],{},"We don't have an image for the exact calculated values. For example, the calculation might give us ",[22,65888,65889],{},"pupil_x = 7.3",", but we only generated images for ",[22,65892,61285],{}," values of ",[22,65895,65896],{},"-15, 0, and 15",". So we use our ",[22,65899,65900],{},"nearest()"," helper function to snap to the closest value we actually have. In this example, 7.3 would snap to 15.",[27,65903,65904,65905,65907,65908,65911],{},"Finally, we construct a key string from the three snapped values and call ",[22,65906,65259],{}," to display it. Because all our images are already preloaded and positioned in exactly the same spot, this swap happens instantly - there's absolutely no flicker or loading delay. It's just an immediate switch from one ",[22,65909,65910],{},"\u003Cimg\u002F>"," element being visible to another.",[104,65913,65915],{"id":65914},"adding-device-tilt-support-for-mobile","Adding Device Tilt Support for Mobile",[27,65917,65918],{},"If you want to make this even cooler on mobile devices, you can add support for device orientation so the face follows the tilt of the phone rather than (or in addition to) the cursor position. Modern smartphones have built-in accelerometers and gyroscopes that can detect how the device is tilted in space.",[27,65920,65921],{},"However, there's an important privacy consideration here - most modern browsers require explicit user permission before they'll allow a website to access device orientation data. This means we need to:",[1003,65923,65924,65934],{},[1006,65925,65926,65927,65930,65931,14105],{},"Serve the page over ",[22,65928,65929],{},"HTTPS"," (not just ",[22,65932,65933],{},"http:\u002F\u002F",[1006,65935,65936],{},"Ask the user to grant permission, typically by having them tap a button",[27,65938,65939],{},"Here's the general flow for implementing this:",[2569,65941,65942,65945,65951,65954,65961],{},[1006,65943,65944],{},"Detect if the user is on a mobile device",[1006,65946,65947,65948],{},"Show a button that says something like ",[22,65949,65950],{},"\"Enable Motion Tracking\"",[1006,65952,65953],{},"When the user taps the button, request permission to access device orientation",[1006,65955,65956,65957,65960],{},"If permission is granted, start listening to ",[22,65958,65959],{},"deviceorientation"," events",[1006,65962,65963,65964,65967,65968,65971,65972,65975],{},"Convert the device's tilt angles (",[22,65965,65966],{},"beta"," for front\u002Fback tilt and ",[22,65969,65970],{},"gamma"," for left\u002Fright tilt) into our normalised ",[22,65973,65974],{},"[-1, 1]"," range, similar to how we handled mouse coordinates",[27,65977,65978],{},"Here's what the code might look like (add this to your script section):",[128,65980,65982],{"className":36884,"code":65981,"language":29196,"meta":133,"style":133},"async function enableMotion() {\n    \u002F\u002F On iOS, we need to explicitly request permission\n    if (typeof DeviceOrientationEvent?.requestPermission === \"function\") {\n        try {\n            const perm = await DeviceOrientationEvent.requestPermission();\n            if (perm !== \"granted\") {\n                alert(\"Permission denied. Motion tracking won't work.\");\n                return;\n            }\n        } catch (err) {\n            console.error(\"Error requesting device orientation permission:\", err);\n            return;\n        }\n    }\n\n    \u002F\u002F Start listening to device orientation changes\n    window.addEventListener(\"deviceorientation\", (event) => {\n        \u002F\u002F beta: front-to-back tilt in degrees, where:\n        \u002F\u002F   0 = device is lying flat\n        \u002F\u002F   positive = top of device tilted away from you\n        \u002F\u002F   negative = top of device tilted toward you\n        const beta = event.beta || 0;\n\n        \u002F\u002F gamma: left-to-right tilt in degrees, where:\n        \u002F\u002F   0 = device is lying flat\n        \u002F\u002F   positive = device tilted to the right\n        \u002F\u002F   negative = device tilted to the left\n        const gamma = event.gamma || 0;\n\n        \u002F\u002F Normalize to -1..1 range (assuming tilt up to 45 degrees in any direction)\n        const normX = Math.max(-1, Math.min(1, gamma \u002F 45));\n        const normY = Math.max(-1, Math.min(1, beta \u002F 45));\n\n        \u002F\u002F Use the same mapping logic as we did for mouse movement\n        const targetPupilX = normX * 15;\n        const targetPupilY = -normY * 15;\n        const targetPitch = normY * 20;\n\n        \u002F\u002F Snap to nearest values and show the image\n        const snapPitch = nearest(targetPitch, pitchValues);\n        const snapPupilX = nearest(targetPupilX, pupilXValues);\n        const snapPupilY = nearest(targetPupilY, pupilYValues);\n\n        const key = `${snapPitch}_${snapPupilX}_${snapPupilY}`;\n        showImage(key);\n    });\n\n    console.log(\"Motion tracking enabled!\");\n}\n",[22,65983,65984,65994,65999,66018,66024,66040,66052,66064,66070,66074,66082,66095,66101,66105,66109,66113,66118,66138,66143,66148,66153,66158,66174,66178,66183,66187,66192,66197,66213,66217,66222,66256,66290,66294,66299,66315,66334,66350,66354,66359,66371,66383,66395,66399,66423,66429,66433,66437,66450],{"__ignoreMap":133},[137,65985,65986,65988,65990,65992],{"class":139,"line":140},[137,65987,15050],{"class":143},[137,65989,154],{"class":143},[137,65991,64356],{"class":147},[137,65993,275],{"class":157},[137,65995,65996],{"class":139,"line":173},[137,65997,65998],{"class":308},"    \u002F\u002F On iOS, we need to explicitly request permission\n",[137,66000,66001,66003,66005,66008,66011,66013,66016],{"class":139,"line":188},[137,66002,24696],{"class":143},[137,66004,158],{"class":157},[137,66006,66007],{"class":143},"typeof",[137,66009,66010],{"class":157}," DeviceOrientationEvent?.requestPermission ",[137,66012,5502],{"class":143},[137,66014,66015],{"class":284}," \"function\"",[137,66017,170],{"class":157},[137,66019,66020,66022],{"class":139,"line":269},[137,66021,15697],{"class":143},[137,66023,256],{"class":157},[137,66025,66026,66028,66030,66032,66034,66036,66038],{"class":139,"line":278},[137,66027,5772],{"class":143},[137,66029,64417],{"class":364},[137,66031,151],{"class":143},[137,66033,15069],{"class":143},[137,66035,64424],{"class":157},[137,66037,64427],{"class":147},[137,66039,924],{"class":157},[137,66041,66042,66044,66046,66048,66050],{"class":139,"line":291},[137,66043,5747],{"class":143},[137,66045,64437],{"class":157},[137,66047,26215],{"class":143},[137,66049,64442],{"class":284},[137,66051,170],{"class":157},[137,66053,66054,66057,66059,66062],{"class":139,"line":297},[137,66055,66056],{"class":147},"                alert",[137,66058,356],{"class":157},[137,66060,66061],{"class":284},"\"Permission denied. Motion tracking won't work.\"",[137,66063,1502],{"class":157},[137,66065,66066,66068],{"class":139,"line":302},[137,66067,5761],{"class":143},[137,66069,3276],{"class":157},[137,66071,66072],{"class":139,"line":662},[137,66073,760],{"class":157},[137,66075,66076,66078,66080],{"class":139,"line":667},[137,66077,15729],{"class":157},[137,66079,2807],{"class":143},[137,66081,50719],{"class":157},[137,66083,66084,66086,66088,66090,66093],{"class":139,"line":786},[137,66085,1493],{"class":157},[137,66087,2812],{"class":147},[137,66089,356],{"class":157},[137,66091,66092],{"class":284},"\"Error requesting device orientation permission:\"",[137,66094,50733],{"class":157},[137,66096,66097,66099],{"class":139,"line":798},[137,66098,4683],{"class":143},[137,66100,3276],{"class":157},[137,66102,66103],{"class":139,"line":803},[137,66104,1966],{"class":157},[137,66106,66107],{"class":139,"line":931},[137,66108,294],{"class":157},[137,66110,66111],{"class":139,"line":1568},[137,66112,516],{"emptyLinePlaceholder":515},[137,66114,66115],{"class":139,"line":1573},[137,66116,66117],{"class":308},"    \u002F\u002F Start listening to device orientation changes\n",[137,66119,66120,66122,66124,66126,66128,66130,66132,66134,66136],{"class":139,"line":1578},[137,66121,65825],{"class":157},[137,66123,4412],{"class":147},[137,66125,356],{"class":157},[137,66127,64471],{"class":284},[137,66129,24531],{"class":157},[137,66131,24689],{"class":161},[137,66133,219],{"class":157},[137,66135,222],{"class":143},[137,66137,256],{"class":157},[137,66139,66140],{"class":139,"line":1588},[137,66141,66142],{"class":308},"        \u002F\u002F beta: front-to-back tilt in degrees, where:\n",[137,66144,66145],{"class":139,"line":1601},[137,66146,66147],{"class":308},"        \u002F\u002F   0 = device is lying flat\n",[137,66149,66150],{"class":139,"line":3802},[137,66151,66152],{"class":308},"        \u002F\u002F   positive = top of device tilted away from you\n",[137,66154,66155],{"class":139,"line":3808},[137,66156,66157],{"class":308},"        \u002F\u002F   negative = top of device tilted toward you\n",[137,66159,66160,66162,66164,66166,66168,66170,66172],{"class":139,"line":3822},[137,66161,3008],{"class":143},[137,66163,64489],{"class":364},[137,66165,151],{"class":143},[137,66167,64494],{"class":157},[137,66169,50706],{"class":143},[137,66171,7687],{"class":364},[137,66173,3276],{"class":157},[137,66175,66176],{"class":139,"line":3827},[137,66177,516],{"emptyLinePlaceholder":515},[137,66179,66180],{"class":139,"line":3832},[137,66181,66182],{"class":308},"        \u002F\u002F gamma: left-to-right tilt in degrees, where:\n",[137,66184,66185],{"class":139,"line":3840},[137,66186,66147],{"class":308},[137,66188,66189],{"class":139,"line":3846},[137,66190,66191],{"class":308},"        \u002F\u002F   positive = device tilted to the right\n",[137,66193,66194],{"class":139,"line":3861},[137,66195,66196],{"class":308},"        \u002F\u002F   negative = device tilted to the left\n",[137,66198,66199,66201,66203,66205,66207,66209,66211],{"class":139,"line":3883},[137,66200,3008],{"class":143},[137,66202,64511],{"class":364},[137,66204,151],{"class":143},[137,66206,64516],{"class":157},[137,66208,50706],{"class":143},[137,66210,7687],{"class":364},[137,66212,3276],{"class":157},[137,66214,66215],{"class":139,"line":3896},[137,66216,516],{"emptyLinePlaceholder":515},[137,66218,66219],{"class":139,"line":3901},[137,66220,66221],{"class":308},"        \u002F\u002F Normalize to -1..1 range (assuming tilt up to 45 degrees in any direction)\n",[137,66223,66224,66226,66228,66230,66232,66234,66236,66238,66240,66242,66244,66246,66248,66250,66252,66254],{"class":139,"line":3906},[137,66225,3008],{"class":143},[137,66227,64544],{"class":364},[137,66229,151],{"class":143},[137,66231,7675],{"class":157},[137,66233,64215],{"class":147},[137,66235,356],{"class":157},[137,66237,8215],{"class":143},[137,66239,6065],{"class":364},[137,66241,64224],{"class":157},[137,66243,64227],{"class":147},[137,66245,356],{"class":157},[137,66247,6065],{"class":364},[137,66249,64567],{"class":157},[137,66251,47],{"class":143},[137,66253,64572],{"class":364},[137,66255,8614],{"class":157},[137,66257,66258,66260,66262,66264,66266,66268,66270,66272,66274,66276,66278,66280,66282,66284,66286,66288],{"class":139,"line":3911},[137,66259,3008],{"class":143},[137,66261,64586],{"class":364},[137,66263,151],{"class":143},[137,66265,7675],{"class":157},[137,66267,64215],{"class":147},[137,66269,356],{"class":157},[137,66271,8215],{"class":143},[137,66273,6065],{"class":364},[137,66275,64224],{"class":157},[137,66277,64227],{"class":147},[137,66279,356],{"class":157},[137,66281,6065],{"class":364},[137,66283,64609],{"class":157},[137,66285,47],{"class":143},[137,66287,64572],{"class":364},[137,66289,8614],{"class":157},[137,66291,66292],{"class":139,"line":4666},[137,66293,516],{"emptyLinePlaceholder":515},[137,66295,66296],{"class":139,"line":4672},[137,66297,66298],{"class":308},"        \u002F\u002F Use the same mapping logic as we did for mouse movement\n",[137,66300,66301,66303,66305,66307,66309,66311,66313],{"class":139,"line":4680},[137,66302,3008],{"class":143},[137,66304,65672],{"class":364},[137,66306,151],{"class":143},[137,66308,64147],{"class":157},[137,66310,7672],{"class":143},[137,66312,65681],{"class":364},[137,66314,3276],{"class":157},[137,66316,66317,66319,66321,66323,66325,66328,66330,66332],{"class":139,"line":4711},[137,66318,3008],{"class":143},[137,66320,65693],{"class":364},[137,66322,151],{"class":143},[137,66324,63685],{"class":143},[137,66326,66327],{"class":157},"normY ",[137,66329,7672],{"class":143},[137,66331,65681],{"class":364},[137,66333,3276],{"class":157},[137,66335,66336,66338,66340,66342,66344,66346,66348],{"class":139,"line":4716},[137,66337,3008],{"class":143},[137,66339,65715],{"class":364},[137,66341,151],{"class":143},[137,66343,64176],{"class":157},[137,66345,7672],{"class":143},[137,66347,60522],{"class":364},[137,66349,3276],{"class":157},[137,66351,66352],{"class":139,"line":4721},[137,66353,516],{"emptyLinePlaceholder":515},[137,66355,66356],{"class":139,"line":4727},[137,66357,66358],{"class":308},"        \u002F\u002F Snap to nearest values and show the image\n",[137,66360,66361,66363,66365,66367,66369],{"class":139,"line":4732},[137,66362,3008],{"class":143},[137,66364,65739],{"class":364},[137,66366,151],{"class":143},[137,66368,65415],{"class":147},[137,66370,65746],{"class":157},[137,66372,66373,66375,66377,66379,66381],{"class":139,"line":5006},[137,66374,3008],{"class":143},[137,66376,65753],{"class":364},[137,66378,151],{"class":143},[137,66380,65415],{"class":147},[137,66382,65760],{"class":157},[137,66384,66385,66387,66389,66391,66393],{"class":139,"line":5014},[137,66386,3008],{"class":143},[137,66388,65767],{"class":364},[137,66390,151],{"class":143},[137,66392,65415],{"class":147},[137,66394,65774],{"class":157},[137,66396,66397],{"class":139,"line":14343},[137,66398,516],{"emptyLinePlaceholder":515},[137,66400,66401,66403,66405,66407,66409,66411,66413,66415,66417,66419,66421],{"class":139,"line":24199},[137,66402,3008],{"class":143},[137,66404,65071],{"class":364},[137,66406,151],{"class":143},[137,66408,4686],{"class":284},[137,66410,65791],{"class":157},[137,66412,65084],{"class":284},[137,66414,65796],{"class":157},[137,66416,65084],{"class":284},[137,66418,65801],{"class":157},[137,66420,4706],{"class":284},[137,66422,3276],{"class":157},[137,66424,66425,66427],{"class":139,"line":24773},[137,66426,65315],{"class":147},[137,66428,65812],{"class":157},[137,66430,66431],{"class":139,"line":24778},[137,66432,2832],{"class":157},[137,66434,66435],{"class":139,"line":24783},[137,66436,516],{"emptyLinePlaceholder":515},[137,66438,66439,66441,66443,66445,66448],{"class":139,"line":24793},[137,66440,493],{"class":157},[137,66442,353],{"class":147},[137,66444,356],{"class":157},[137,66446,66447],{"class":284},"\"Motion tracking enabled!\"",[137,66449,1502],{"class":157},[137,66451,66452],{"class":139,"line":24827},[137,66453,510],{"class":157},[27,66455,66456],{},"Then, in your HTML body, you could add a button like this:",[128,66458,66460],{"className":4024,"code":66459,"language":4026,"meta":133,"style":133},"\u003Cbutton onclick=\"enableMotion()\" style=\"position: fixed; top: 20px; left: 20px; z-index: 1000;\">\n    Enable Motion Tracking\n\u003C\u002Fbutton>\n",[22,66461,66462,66490,66495],{"__ignoreMap":133},[137,66463,66464,66466,66468,66471,66473,66475,66478,66481,66483,66485,66488],{"class":139,"line":140},[137,66465,4033],{"class":157},[137,66467,8170],{"class":4036},[137,66469,66470],{"class":147}," onclick",[137,66472,253],{"class":157},[137,66474,28792],{"class":284},[137,66476,66477],{"class":147},"enableMotion",[137,66479,66480],{"class":284},"()\"",[137,66482,3755],{"class":147},[137,66484,253],{"class":157},[137,66486,66487],{"class":284},"\"position: fixed; top: 20px; left: 20px; z-index: 1000;\"",[137,66489,4053],{"class":157},[137,66491,66492],{"class":139,"line":173},[137,66493,66494],{"class":157},"    Enable Motion Tracking\n",[137,66496,66497,66499,66501],{"class":139,"line":188},[137,66498,4083],{"class":157},[137,66500,8170],{"class":4036},[137,66502,4053],{"class":157},[27,66504,66505],{},"When the user taps this button on their phone, they'll see a permission dialog, and once they grant permission, the face will start following the tilt of their device. It's a really cool effect when you're showing this to friends - they can physically tilt their phone around and watch the face track their movements!",[104,66507,2567],{"id":2566},[27,66509,66510,66511,1017],{},"I hope this detailed walkthrough has given you the confidence to build your own version of this effect or apply these techniques to other creative projects. Have fun experimenting, and feel free to share what you create! To explore the full implementation, styling, and code structure, check out the complete project on GitHub ",[45,66512,10647],{"href":66513,"target":2716,"rel":66514},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Finteractive-face-eye-tracker",[2718,2719],[2617,66516,66517],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":133,"searchDepth":173,"depth":173,"links":66519},[66520,66521,66522,66523,66530,66531,66532,66533,66534],{"id":61178,"depth":173,"text":61179},{"id":61229,"depth":173,"text":61230},{"id":61333,"depth":173,"text":61334},{"id":61471,"depth":173,"text":61472,"children":66524},[66525,66526,66527,66528,66529],{"id":61478,"depth":188,"text":61479},{"id":61613,"depth":188,"text":61614},{"id":61704,"depth":188,"text":61705},{"id":61898,"depth":188,"text":61899},{"id":62398,"depth":188,"text":62399},{"id":62648,"depth":173,"text":62649},{"id":64793,"depth":173,"text":64794},{"id":65353,"depth":173,"text":65354},{"id":65914,"depth":173,"text":65915},{"id":2566,"depth":173,"text":2567},"Learn how to create an interactive face-tracking effect using AI-generated images. This tutorial covers generating face variations with the fofr\u002Fexpression-editor model on Replicate, building a Node.js script to automate image generation, and implementing smooth cursor and device motion tracking in the browser using vanilla HTML, CSS, and JavaScript.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1766137601\u002Fblog\u002Fmaking-a-face-follow-your-cursor-with-ai%E2%80%91generated-images\u002Fmaking-a-face-follow-your-cursor-with-ai_generated-images_hejbzy",[66538,66539,66540,66541,66542,66543,66544,66545,66546,66547,66548,66549,66550,66551,66552],"face tracking cursor","AI generated images","expression editor model","Replicate API","fofr expression editor","interactive face effect","cursor tracking JavaScript","device orientation tracking","Node.js image generation","face following mouse","eye tracking web effect","pre-generated image swap","TypeScript Replicate tutorial","mobile motion tracking","interactive web experience",{},"\u002F2025\u002F12\u002F20\u002Fmaking-a-face-follow-your-cursor-with-ai-generated-images","20th December 2025",{"title":61088,"description":66535},"2025\u002F12\u002F20\u002Fmaking-a-face-follow-your-cursor-with-ai-generated-images","QaVhOD3VRuaMCOMxvbEWL64ouadC5T2frVCWptIGCJc",{"id":66560,"title":66561,"articleTags":66562,"author":11,"blog":12,"body":66563,"description":71423,"extension":2649,"image":71424,"keywords":71425,"meta":71439,"navigation":515,"path":71440,"published":71441,"readTime":667,"seo":71442,"stem":71443,"type":2662,"__hash__":71444},"content\u002F2025\u002F12\u002F29\u002Fbuilding-content-adaptive-interfaces-with-googles-a2ui.md","Building Content-Adaptive Interfaces with Google's A2UI",[27886,10,12817],{"type":14,"value":66564,"toc":71390},[66565,66568,66582,66584,66588,66593,66608,66611,66620,66627,66633,66644,66647,66651,66656,66694,66698,66705,66708,66714,66724,66727,66731,66734,66744,66763,66770,66774,66777,66786,66790,66793,66796,66810,66813,66816,66821,66826,66832,66850,66861,66899,66905,66909,66919,66924,67170,67176,67183,67189,67211,67217,67249,67255,67281,67319,67690,67697,67703,67710,67714,67717,67962,67969,67973,67976,67995,67998,68316,68320,68323,68527,68530,68534,68537,68809,68816,68822,68829,69290,69293,69316,69319,69348,69354,69361,69365,69684,69696,69700,69705,70034,70037,70041,70048,70385,70388,70392,70395,70453,70456,70462,70469,70863,70866,70901,70914,70920,70927,70933,71114,71117,71142,71145,71167,71170,71176,71182,71258,71263,71269,71276,71282,71294,71297,71314,71319,71350,71357,71361,71364,71371,71380,71387],[17,66566,66561],{"id":66567},"building-content-adaptive-interfaces-with-googles-a2ui",[27,66569,66570],{},[30,66571,66572,36,66574,40,66576],{},[33,66573],{"value":35},[33,66575],{"value":39},[42,66577,66578],{},[45,66579,66580],{"href":47},[33,66581],{"value":50},[52,66583],{":tags":54},[56,66585],{":audio-src":66586,":transcript-src":66587},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F12\u002F29\u002Fbuilding-content-adaptive-interfaces-with-googles-a2ui\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2025\u002F12\u002F29\u002Fbuilding-content-adaptive-interfaces-with-googles-a2ui\u002Fsummary.json",[27,66589,66590],{},[63,66591],{"alt":12847,"src":66592},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1766977382\u002Fblog\u002Fbuilding-content-adaptive-interfaces-with-googles-a2ui\u002Fhero-building-content-adaptive-interfaces-with-googles-a2ui_jc2fg2",[3244,66594,66595],{},[27,66596,66597],{},[42,66598,66599,66600,66603,66604,66607],{},"How to build adaptive interfaces where the AI decides not just ",[30,66601,66602],{},"what"," to show, but ",[30,66605,66606],{},"how"," to style it.",[27,66609,66610],{},"What if your UI could automatically adapt its look and feel based on the content it displays? Imagine a movie recommendation site that understands your viewing habits. If you enjoy slow, tense films at night, it uses dark colours, calm animations, and layouts that emphasise mood and detailed descriptions. If you prefer browsing quickly across genres during the day, it shifts to vibrant colours, compact cards, and faster interactions - all without hard-coded UI rules.",[27,66612,66613,66614,66619],{},"With today's LLMs, we can build interfaces that understand context and adapt accordingly. Google recently showcased this capability with their ",[45,66615,66618],{"href":66616,"target":2716,"rel":66617},"https:\u002F\u002Fdevelopers.googleblog.com\u002Fintroducing-a2ui-an-open-project-for-agent-driven-interfaces\u002F",[2718,2719],"A2UI"," (Agent-to-UI) open-source project, and I'm excited to show you how it works.",[27,66621,66622,66623,66626],{},"In this article, we'll build a content-driven blog application. Each article renders with a unique layout and styling determined entirely by ",[42,66624,66625],{},"Gemini 2.5 Flash",". Unlike traditional approaches where UI is hardcoded, A2UI lets an AI agent analyse content and dynamically decide how to render it.",[104,66628,66630],{"id":66629},"what-is-a2ui",[42,66631,66632],{},"What is A2UI?",[27,66634,66635,66639,66640,66643],{},[45,66636,66618],{"href":66637,"target":2716,"rel":66638},"https:\u002F\u002Fa2ui.org\u002F",[2718,2719]," stands for ",[42,66641,66642],{},"Agent-to-UI"," - an open-source protocol released by Google under the Apache 2 license. It defines a standard way for AI agents to describe user interfaces without executing code directly in your application.",[27,66645,66646],{},"A2UI uses a declarative approach: agents send JSON descriptions of UI components, and your application maps these to trusted components from your design system. The protocol supports streaming, enabling progressive rendering so users see results appear in real time as the AI generates them. A2UI is completely framework-agnostic, providing a standard interface that works seamlessly with Lit, Angular, React, Flutter, or any other framework.",[123,66648,66650],{"id":66649},"whats-happening-under-the-hood","What's Happening Under the Hood",[27,66652,66653],{},[63,66654],{"alt":66650,"src":66655},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1766977381\u002Fblog\u002Fbuilding-content-adaptive-interfaces-with-googles-a2ui\u002Fwhats-happening-under-the-hood_xtlzvk",[2569,66657,66658,66664,66670,66676,66682,66688],{},[1006,66659,66660,66663],{},[42,66661,66662],{},"You send a message"," through the web UI",[1006,66665,66666,66669],{},[42,66667,66668],{},"The A2A agent"," receives it and forwards the conversation to Gemini",[1006,66671,66672,66675],{},[42,66673,66674],{},"Gemini generates"," A2UI JSON messages that describe the UI",[1006,66677,66678,66681],{},[42,66679,66680],{},"The A2A agent streams"," these messages back to the web app",[1006,66683,66684,66687],{},[42,66685,66686],{},"The A2UI renderer"," converts them into native web components",[1006,66689,66690,66693],{},[42,66691,66692],{},"You see the UI"," rendered in your browser",[104,66695,66697],{"id":66696},"where-content-adaptive-interfaces-make-the-most-sense","Where Content-Adaptive Interfaces Make the Most Sense",[27,66699,66700,66701,66704],{},"At this point, you're probably asking a reasonable question: ",[42,66702,66703],{},"Why would we ever let an LLM decide what to render?"," Isn't this overkill when we've been building UIs with hard-coded conditionals for decades - faster, simpler, and perfectly adequate in most cases? And honestly, you wouldn't be wrong.",[27,66706,66707],{},"Not every interface should be replaced with a content-adaptive one. Most UIs don't need this at all. Traditional, deterministic UI logic is still the right choice for most applications.",[27,66709,66710,66711,66713],{},"But that's not the problem ",[42,66712,66618],{}," is trying to solve.",[27,66715,66716,66717,66720,66721],{},"A2UI shines when content is unpredictable, high-variance, or semantically rich - where predefining every possible UI state becomes brittle, complex, or unrealistic. Instead of asking ",[30,66718,66719],{},"\"Which UI should I render?\""," ahead of time, you let the agent decide ",[30,66722,66723],{},"\"What is the most helpful interface for this content, right now?\"",[27,66725,66726],{},"Here are two scenarios where that trade-off makes sense.",[123,66728,66730],{"id":66729},"_1-conversational-assistants","1. Conversational Assistants",[27,66732,66733],{},"Most chatbots still default to plain text responses, even when a structured interface would be far more helpful.",[27,66735,66736,66737,66740,66741],{},"Imagine a conversational assistant that helps you book a restaurant. Instead of responding with instructions like ",[30,66738,66739],{},"\"Go to this website and fill out this form,\""," the agent can dynamically render the ",[42,66742,66743],{},"appropriate UI component:",[1003,66745,66746,66753,66760],{},[1006,66747,66748,66749,66752],{},"Ask for nearby restaurants → render a ",[42,66750,66751],{},"card-based list"," with images, ratings, and availability",[1006,66754,66755,66756,66759],{},"Ask to book a table for two → render a ",[42,66757,66758],{},"booking form"," with date and time selectors",[1006,66761,66762],{},"Ask to change the time → update only the relevant UI fields",[27,66764,66765,66766,66769],{},"With A2UI, the chatbot doesn't just respond - it ",[42,66767,66768],{},"assembles the interface"," that best matches the user's intent.",[123,66771,66773],{"id":66772},"_2-highly-personalised-experiences","2. Highly Personalised Experiences",[27,66775,66776],{},"Personalisation is one of the strongest arguments for content-adaptive interfaces.",[27,66778,66779,66780,66782,66783,66785],{},"As an application learns from user behaviour - what they click, how they navigate, what they ignore - an agent can adjust not just ",[30,66781,66602],{}," content appears, but ",[30,66784,66606],{}," it's presented. Instead of building and maintaining multiple UI variants, the agent decides layout, emphasis, and component choice on the fly.",[104,66787,66788],{"id":59180},[42,66789,59181],{},[27,66791,66792],{},"To demonstrate A2UI's power, we'll create a blog application.",[27,66794,66795],{},"Each article is written in Markdown. When you view it, Gemini analyses the content and decides:",[2569,66797,66798,66804],{},[1006,66799,66800,66803],{},[42,66801,66802],{},"Which components to use"," (hero sections, code blocks, image galleries, quotes, etc.)",[1006,66805,66806,66809],{},[42,66807,66808],{},"How to style them"," (colours, spacing, shadows based on the article's topic)",[27,66811,66812],{},"Here's how it works: When a user opens an article, the client sends the article data to the A2UI agent. Gemini analyses the content - understanding its structure, intent, and visual tone - then determines which UI components and styles fit best. The agent returns this as A2UI JSON, which the Lit renderer translates into native web components. The result is a polished, content-appropriate interface rendered instantly in the browser.",[27,66814,66815],{},"The same rendering system produces completely different layouts for a technology article, a cooking guide, or a photography guide.",[27,66817,66818],{},[63,66819],{"alt":59181,"src":66820},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1766977381\u002Fblog\u002Fbuilding-content-adaptive-interfaces-with-googles-a2ui\u002Fwhat-we-are-building_eirhsy",[3244,66822,66823],{},[27,66824,66825],{},"Before continuing, feel free to clone the repo, run it locally, and skip ahead - or keep reading for a full walkthrough.",[123,66827,66829],{"id":66828},"prerequisites",[42,66830,66831],{},"Prerequisites",[1003,66833,66834,66839],{},[1006,66835,66836,66838],{},[42,66837,2669],{}," (v24 or later)",[1006,66840,66841,66844,66845],{},[42,66842,66843],{},"A Gemini API key","—Get one free from ",[45,66846,66849],{"href":66847,"target":2716,"rel":66848},"https:\u002F\u002Faistudio.google.com\u002Fwelcome",[2718,2719],"Google AI Studio",[3244,66851,66852],{},[27,66853,66854,66855,2107,66858,66860],{},"Make sure to add the ",[42,66856,66857],{},"Gemini API key",[22,66859,13489],{}," file before starting the app.",[128,66862,66864],{"className":8665,"code":66863,"language":8667,"meta":133,"style":133},"git clone https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fa2ui-content-demo.git\ncp .env.example .env\nnpm install\nnpm run dev\n",[22,66865,66866,66875,66885,66891],{"__ignoreMap":133},[137,66867,66868,66870,66872],{"class":139,"line":140},[137,66869,47968],{"class":147},[137,66871,47971],{"class":284},[137,66873,66874],{"class":284}," https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fa2ui-content-demo.git\n",[137,66876,66877,66880,66883],{"class":139,"line":173},[137,66878,66879],{"class":147},"cp",[137,66881,66882],{"class":284}," .env.example",[137,66884,61410],{"class":284},[137,66886,66887,66889],{"class":139,"line":188},[137,66888,9536],{"class":147},[137,66890,9571],{"class":284},[137,66892,66893,66895,66897],{"class":139,"line":269},[137,66894,9536],{"class":147},[137,66896,9578],{"class":284},[137,66898,9581],{"class":284},[27,66900,66901,66902],{},"The website will run on ",[22,66903,66904],{},"http:\u002F\u002Flocalhost:5173",[104,66906,66907],{"id":47891},[42,66908,47892],{},[3244,66910,66911],{},[27,66912,66913,66914,66918],{},"For simplicity - and to avoid an overly long blog post - some examples and setup steps are abbreviated here. For full integration details, refer to the original ",[45,66915,61246],{"href":66916,"target":2716,"rel":66917},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fa2ui-content-demo",[2718,2719],". focus on explaining the core concepts without excessive detail, keeping things digestible.",[27,66920,66921],{},[42,66922,66923],{},"High-Level Overview of the Project Structure",[128,66925,66927],{"className":8665,"code":66926,"language":8667,"meta":133,"style":133},"a2ui-content-demo\u002F\n├── ...\n├── .env                          # API key\n├── index.html                    # Entry point\n├── public\u002F\n│   └── articles\u002F                 # Markdown content\n│       ├── space-exploration.md\n│       ├── web-components.md\n│       └── italian-cooking.md\n├── server\u002F\n│   ├── index.ts                  # A2UI Agent (Node.js + Gemini)\n│   ├── tsconfig.json\n│   └── a2ui-schema.json          # Component definitions\n└── src\u002F\n    ├── app.ts                    # Main application\n    ├── types.ts                  # Type definitions\n    ├── components\u002F               # Lit web components\n    │   ├── a2ui-renderer.ts      # Main renderer\n    │   ├── a2ui-hero.ts\n    │   ├── a2ui-text-block.ts\n    │   ├── a2ui-code-block.ts\n    │   └── ...\n    └── services\u002F\n        ├── a2ui-client.ts        # Client for A2UI agent\n        └── content-parser.ts     # Markdown parser\n",[22,66928,66929,66934,66942,66952,66962,66969,66983,66993,67002,67012,67019,67032,67041,67053,67061,67072,67082,67092,67105,67114,67123,67132,67140,67148,67159],{"__ignoreMap":133},[137,66930,66931],{"class":139,"line":140},[137,66932,66933],{"class":147},"a2ui-content-demo\u002F\n",[137,66935,66936,66939],{"class":139,"line":173},[137,66937,66938],{"class":147},"├──",[137,66940,66941],{"class":284}," ...\n",[137,66943,66944,66946,66949],{"class":139,"line":188},[137,66945,66938],{"class":147},[137,66947,66948],{"class":284}," .env",[137,66950,66951],{"class":308},"                          # API key\n",[137,66953,66954,66956,66959],{"class":139,"line":269},[137,66955,66938],{"class":147},[137,66957,66958],{"class":284}," index.html",[137,66960,66961],{"class":308},"                    # Entry point\n",[137,66963,66964,66966],{"class":139,"line":278},[137,66965,66938],{"class":147},[137,66967,66968],{"class":284}," public\u002F\n",[137,66970,66971,66974,66977,66980],{"class":139,"line":291},[137,66972,66973],{"class":147},"│",[137,66975,66976],{"class":284},"   └──",[137,66978,66979],{"class":284}," articles\u002F",[137,66981,66982],{"class":308},"                 # Markdown content\n",[137,66984,66985,66987,66990],{"class":139,"line":297},[137,66986,66973],{"class":147},[137,66988,66989],{"class":284},"       ├──",[137,66991,66992],{"class":284}," space-exploration.md\n",[137,66994,66995,66997,66999],{"class":139,"line":302},[137,66996,66973],{"class":147},[137,66998,66989],{"class":284},[137,67000,67001],{"class":284}," web-components.md\n",[137,67003,67004,67006,67009],{"class":139,"line":662},[137,67005,66973],{"class":147},[137,67007,67008],{"class":284},"       └──",[137,67010,67011],{"class":284}," italian-cooking.md\n",[137,67013,67014,67016],{"class":139,"line":667},[137,67015,66938],{"class":147},[137,67017,67018],{"class":284}," server\u002F\n",[137,67020,67021,67023,67026,67029],{"class":139,"line":786},[137,67022,66973],{"class":147},[137,67024,67025],{"class":284},"   ├──",[137,67027,67028],{"class":284}," index.ts",[137,67030,67031],{"class":308},"                  # A2UI Agent (Node.js + Gemini)\n",[137,67033,67034,67036,67038],{"class":139,"line":798},[137,67035,66973],{"class":147},[137,67037,67025],{"class":284},[137,67039,67040],{"class":284}," tsconfig.json\n",[137,67042,67043,67045,67047,67050],{"class":139,"line":803},[137,67044,66973],{"class":147},[137,67046,66976],{"class":284},[137,67048,67049],{"class":284}," a2ui-schema.json",[137,67051,67052],{"class":308},"          # Component definitions\n",[137,67054,67055,67058],{"class":139,"line":931},[137,67056,67057],{"class":147},"└──",[137,67059,67060],{"class":284}," src\u002F\n",[137,67062,67063,67066,67069],{"class":139,"line":1568},[137,67064,67065],{"class":147},"    ├──",[137,67067,67068],{"class":284}," app.ts",[137,67070,67071],{"class":308},"                    # Main application\n",[137,67073,67074,67076,67079],{"class":139,"line":1573},[137,67075,67065],{"class":147},[137,67077,67078],{"class":284}," types.ts",[137,67080,67081],{"class":308},"                  # Type definitions\n",[137,67083,67084,67086,67089],{"class":139,"line":1578},[137,67085,67065],{"class":147},[137,67087,67088],{"class":284}," components\u002F",[137,67090,67091],{"class":308},"               # Lit web components\n",[137,67093,67094,67097,67099,67102],{"class":139,"line":1588},[137,67095,67096],{"class":147},"    │",[137,67098,67025],{"class":284},[137,67100,67101],{"class":284}," a2ui-renderer.ts",[137,67103,67104],{"class":308},"      # Main renderer\n",[137,67106,67107,67109,67111],{"class":139,"line":1601},[137,67108,67096],{"class":147},[137,67110,67025],{"class":284},[137,67112,67113],{"class":284}," a2ui-hero.ts\n",[137,67115,67116,67118,67120],{"class":139,"line":3802},[137,67117,67096],{"class":147},[137,67119,67025],{"class":284},[137,67121,67122],{"class":284}," a2ui-text-block.ts\n",[137,67124,67125,67127,67129],{"class":139,"line":3808},[137,67126,67096],{"class":147},[137,67128,67025],{"class":284},[137,67130,67131],{"class":284}," a2ui-code-block.ts\n",[137,67133,67134,67136,67138],{"class":139,"line":3822},[137,67135,67096],{"class":147},[137,67137,66976],{"class":284},[137,67139,66941],{"class":284},[137,67141,67142,67145],{"class":139,"line":3827},[137,67143,67144],{"class":147},"    └──",[137,67146,67147],{"class":284}," services\u002F\n",[137,67149,67150,67153,67156],{"class":139,"line":3832},[137,67151,67152],{"class":147},"        ├──",[137,67154,67155],{"class":284}," a2ui-client.ts",[137,67157,67158],{"class":308},"        # Client for A2UI agent\n",[137,67160,67161,67164,67167],{"class":139,"line":3840},[137,67162,67163],{"class":147},"        └──",[137,67165,67166],{"class":284}," content-parser.ts",[137,67168,67169],{"class":308},"     # Markdown parser\n",[104,67171,67173],{"id":67172},"define-the-component-schema",[42,67174,67175],{},"Define the Component Schema",[27,67177,67178,67179,67182],{},"The schema (",[22,67180,67181],{},"server\u002Fa2ui-schema.json",") defines which UI components the agent can generate- think of it as the \"vocabulary\" the agent uses to describe UIs.",[27,67184,67185,67188],{},[42,67186,67187],{},"Layout components"," for structure:",[1003,67190,67191,67200,67205],{},[1006,67192,67193,67195,67196,67199],{},[22,67194,31408],{}," \u002F ",[22,67197,67198],{},"Row","- vertical\u002Fhorizontal arrangement with gap and alignment",[1006,67201,67202,67204],{},[22,67203,30584],{},"- container with optional title, content, and image",[1006,67206,67207,67210],{},[22,67208,67209],{},"Divider","- visual separator",[27,67212,67213,67216],{},[42,67214,67215],{},"Content components"," for information:",[1003,67218,67219,67225,67231,67237,67243],{},[1006,67220,67221,67224],{},[22,67222,67223],{},"TextBlock","- markdown\u002Ftext with variants (body, lead, small)",[1006,67226,67227,67230],{},[22,67228,67229],{},"ImageGallery","- grid of images (1–4 columns, with lightbox)",[1006,67232,67233,67236],{},[22,67234,67235],{},"CodeBlock","- syntax-highlighted code with language and line numbers",[1006,67238,67239,67242],{},[22,67240,67241],{},"Quote","- block-quote with author attribution",[1006,67244,67245,67248],{},[22,67246,67247],{},"Table","- data tables with headers and rows",[27,67250,67251,67254],{},[42,67252,67253],{},"Specialized components"," for context:",[1003,67256,67257,67263,67269,67275],{},[1006,67258,67259,67262],{},[22,67260,67261],{},"HeroSection","- full-width header with image overlay",[1006,67264,67265,67268],{},[22,67266,67267],{},"Callout","- highlighted info boxes (info\u002Fwarning\u002Fsuccess\u002Ftip)",[1006,67270,67271,67274],{},[22,67272,67273],{},"Metadata","- article metadata (author, date, tags, read time)",[1006,67276,67277,67280],{},[22,67278,67279],{},"List","- ordered or unordered lists",[27,67282,67283,67284,158,67287,67289,67290,67292,67293,158,67296,67299,67300,164,67303,164,67306,29088,67309,67312,67313,3955,67316,1017],{},"Each component definition specifies ",[42,67285,67286],{},"what's required",[22,67288,67223],{}," needs content, ",[22,67291,67229],{}," needs images) and ",[42,67294,67295],{},"what values are valid",[22,67297,67298],{},"Callout.type"," can only be ",[22,67301,67302],{},"\"info\"",[22,67304,67305],{},"\"warning\"",[22,67307,67308],{},"\"success\"",[22,67310,67311],{},"\"tip\"","). This prevents the agent from generating invalid UI like ",[22,67314,67315],{},"columns: 99",[22,67317,67318],{},"type: \"danger\"",[128,67320,67322],{"className":5155,"code":67321,"language":5157,"meta":133,"style":133},"{\n    \"definitions\": {\n        \"HeroSection\": {\n            \"required\": [\"title\"],\n            \"properties\": {\n                \"title\": { \"type\": \"string\" },\n                \"subtitle\": { \"type\": \"string\" },\n                \"imageUrl\": { \"type\": \"string\" },\n                \"height\": { \"enum\": [\"small\", \"medium\", \"large\", \"full\"] }\n            }\n        },\n        \"ImageGallery\": {\n            \"required\": [\"images\"],\n            \"properties\": {\n                \"images\": {\n                    \"type\": \"array\",\n                    \"items\": { \"properties\": { \"url\": {}, \"caption\": {}, \"alt\": {} } }\n                },\n                \"columns\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 4 },\n                \"lightbox\": { \"type\": \"boolean\", \"default\": true }\n            }\n        },\n        \"Callout\": {\n            \"required\": [\"content\"],\n            \"properties\": {\n                \"content\": { \"type\": \"string\" },\n                \"type\": { \"enum\": [\"info\", \"warning\", \"success\", \"tip\"] },\n                \"title\": { \"type\": \"string\" }\n            }\n        }\n    }\n}\n",[22,67323,67324,67328,67335,67342,67354,67361,67378,67393,67408,67441,67445,67449,67456,67467,67473,67480,67492,67521,67525,67560,67585,67589,67593,67600,67611,67617,67632,67660,67674,67678,67682,67686],{"__ignoreMap":133},[137,67325,67326],{"class":139,"line":140},[137,67327,15971],{"class":157},[137,67329,67330,67333],{"class":139,"line":173},[137,67331,67332],{"class":364},"    \"definitions\"",[137,67334,1819],{"class":157},[137,67336,67337,67340],{"class":139,"line":188},[137,67338,67339],{"class":364},"        \"HeroSection\"",[137,67341,1819],{"class":157},[137,67343,67344,67347,67349,67352],{"class":139,"line":269},[137,67345,67346],{"class":364},"            \"required\"",[137,67348,29669],{"class":157},[137,67350,67351],{"class":284},"\"title\"",[137,67353,21916],{"class":157},[137,67355,67356,67359],{"class":139,"line":278},[137,67357,67358],{"class":364},"            \"properties\"",[137,67360,1819],{"class":157},[137,67362,67363,67366,67368,67371,67373,67376],{"class":139,"line":291},[137,67364,67365],{"class":364},"                \"title\"",[137,67367,54941],{"class":157},[137,67369,67370],{"class":364},"\"type\"",[137,67372,726],{"class":157},[137,67374,67375],{"class":284},"\"string\"",[137,67377,31293],{"class":157},[137,67379,67380,67383,67385,67387,67389,67391],{"class":139,"line":297},[137,67381,67382],{"class":364},"                \"subtitle\"",[137,67384,54941],{"class":157},[137,67386,67370],{"class":364},[137,67388,726],{"class":157},[137,67390,67375],{"class":284},[137,67392,31293],{"class":157},[137,67394,67395,67398,67400,67402,67404,67406],{"class":139,"line":302},[137,67396,67397],{"class":364},"                \"imageUrl\"",[137,67399,54941],{"class":157},[137,67401,67370],{"class":364},[137,67403,726],{"class":157},[137,67405,67375],{"class":284},[137,67407,31293],{"class":157},[137,67409,67410,67413,67415,67418,67420,67423,67425,67428,67430,67433,67435,67438],{"class":139,"line":662},[137,67411,67412],{"class":364},"                \"height\"",[137,67414,54941],{"class":157},[137,67416,67417],{"class":364},"\"enum\"",[137,67419,29669],{"class":157},[137,67421,67422],{"class":284},"\"small\"",[137,67424,164],{"class":157},[137,67426,67427],{"class":284},"\"medium\"",[137,67429,164],{"class":157},[137,67431,67432],{"class":284},"\"large\"",[137,67434,164],{"class":157},[137,67436,67437],{"class":284},"\"full\"",[137,67439,67440],{"class":157},"] }\n",[137,67442,67443],{"class":139,"line":667},[137,67444,760],{"class":157},[137,67446,67447],{"class":139,"line":786},[137,67448,2084],{"class":157},[137,67450,67451,67454],{"class":139,"line":798},[137,67452,67453],{"class":364},"        \"ImageGallery\"",[137,67455,1819],{"class":157},[137,67457,67458,67460,67462,67465],{"class":139,"line":803},[137,67459,67346],{"class":364},[137,67461,29669],{"class":157},[137,67463,67464],{"class":284},"\"images\"",[137,67466,21916],{"class":157},[137,67468,67469,67471],{"class":139,"line":931},[137,67470,67358],{"class":364},[137,67472,1819],{"class":157},[137,67474,67475,67478],{"class":139,"line":1568},[137,67476,67477],{"class":364},"                \"images\"",[137,67479,1819],{"class":157},[137,67481,67482,67485,67487,67490],{"class":139,"line":1573},[137,67483,67484],{"class":364},"                    \"type\"",[137,67486,726],{"class":157},[137,67488,67489],{"class":284},"\"array\"",[137,67491,1961],{"class":157},[137,67493,67494,67497,67499,67502,67504,67507,67510,67513,67515,67518],{"class":139,"line":1578},[137,67495,67496],{"class":364},"                    \"items\"",[137,67498,54941],{"class":157},[137,67500,67501],{"class":364},"\"properties\"",[137,67503,54941],{"class":157},[137,67505,67506],{"class":364},"\"url\"",[137,67508,67509],{"class":157},": {}, ",[137,67511,67512],{"class":364},"\"caption\"",[137,67514,67509],{"class":157},[137,67516,67517],{"class":364},"\"alt\"",[137,67519,67520],{"class":157},": {} } }\n",[137,67522,67523],{"class":139,"line":1588},[137,67524,15680],{"class":157},[137,67526,67527,67530,67532,67534,67536,67539,67541,67544,67546,67548,67550,67553,67555,67558],{"class":139,"line":1601},[137,67528,67529],{"class":364},"                \"columns\"",[137,67531,54941],{"class":157},[137,67533,67370],{"class":364},[137,67535,726],{"class":157},[137,67537,67538],{"class":284},"\"integer\"",[137,67540,164],{"class":157},[137,67542,67543],{"class":364},"\"minimum\"",[137,67545,726],{"class":157},[137,67547,6065],{"class":364},[137,67549,164],{"class":157},[137,67551,67552],{"class":364},"\"maximum\"",[137,67554,726],{"class":157},[137,67556,67557],{"class":364},"4",[137,67559,31293],{"class":157},[137,67561,67562,67565,67567,67569,67571,67574,67576,67579,67581,67583],{"class":139,"line":3802},[137,67563,67564],{"class":364},"                \"lightbox\"",[137,67566,54941],{"class":157},[137,67568,67370],{"class":364},[137,67570,726],{"class":157},[137,67572,67573],{"class":284},"\"boolean\"",[137,67575,164],{"class":157},[137,67577,67578],{"class":364},"\"default\"",[137,67580,726],{"class":157},[137,67582,3097],{"class":364},[137,67584,1115],{"class":157},[137,67586,67587],{"class":139,"line":3808},[137,67588,760],{"class":157},[137,67590,67591],{"class":139,"line":3822},[137,67592,2084],{"class":157},[137,67594,67595,67598],{"class":139,"line":3827},[137,67596,67597],{"class":364},"        \"Callout\"",[137,67599,1819],{"class":157},[137,67601,67602,67604,67606,67609],{"class":139,"line":3832},[137,67603,67346],{"class":364},[137,67605,29669],{"class":157},[137,67607,67608],{"class":284},"\"content\"",[137,67610,21916],{"class":157},[137,67612,67613,67615],{"class":139,"line":3840},[137,67614,67358],{"class":364},[137,67616,1819],{"class":157},[137,67618,67619,67622,67624,67626,67628,67630],{"class":139,"line":3846},[137,67620,67621],{"class":364},"                \"content\"",[137,67623,54941],{"class":157},[137,67625,67370],{"class":364},[137,67627,726],{"class":157},[137,67629,67375],{"class":284},[137,67631,31293],{"class":157},[137,67633,67634,67637,67639,67641,67643,67645,67647,67649,67651,67653,67655,67657],{"class":139,"line":3861},[137,67635,67636],{"class":364},"                \"type\"",[137,67638,54941],{"class":157},[137,67640,67417],{"class":364},[137,67642,29669],{"class":157},[137,67644,67302],{"class":284},[137,67646,164],{"class":157},[137,67648,67305],{"class":284},[137,67650,164],{"class":157},[137,67652,67308],{"class":284},[137,67654,164],{"class":157},[137,67656,67311],{"class":284},[137,67658,67659],{"class":157},"] },\n",[137,67661,67662,67664,67666,67668,67670,67672],{"class":139,"line":3883},[137,67663,67365],{"class":364},[137,67665,54941],{"class":157},[137,67667,67370],{"class":364},[137,67669,726],{"class":157},[137,67671,67375],{"class":284},[137,67673,1115],{"class":157},[137,67675,67676],{"class":139,"line":3896},[137,67677,760],{"class":157},[137,67679,67680],{"class":139,"line":3901},[137,67681,1966],{"class":157},[137,67683,67684],{"class":139,"line":3906},[137,67685,294],{"class":157},[137,67687,67688],{"class":139,"line":3911},[137,67689,510],{"class":157},[27,67691,67692,67693,67696],{},"The agent doesn't ",[30,67694,67695],{},"implement"," these components - it describes which ones to use and with what properties. Your client handles the actual rendering.",[104,67698,67700],{"id":67699},"build-the-a2ui-agent-backend",[42,67701,67702],{},"Build the A2UI Agent (Backend)",[27,67704,67705,67706,67709],{},"The agent is a simple Express server in ",[22,67707,67708],{},"server\u002Findex.ts",". It receives article content and sends it to Gemini with a system prompt that generates A2UI component JSON.",[123,67711,67713],{"id":67712},"setting-up-the-server","Setting Up the Server",[27,67715,67716],{},"First, set up Express and initialise the Gemini client:",[128,67718,67720],{"className":50379,"code":67719,"language":29200,"meta":133,"style":133},"import express, { Request, Response } from \"express\";\nimport cors from \"cors\";\nimport { GoogleGenerativeAI } from \"@google\u002Fgenerative-ai\";\nimport { readFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\n\n\u002F\u002F Load A2UI schema\nconst A2UI_SCHEMA = JSON.parse(readFileSync(join(__dirname, \"a2ui-schema.json\"), \"utf-8\"));\n\n\u002F\u002F Initialize Gemini\nconst genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || \"\");\nconst model = genAI.getGenerativeModel({\n    model: \"gemini-2.5-flash\",\n    generationConfig: {\n        responseMimeType: \"application\u002Fjson\",\n    },\n});\n\nconst app = express();\napp.use(cors());\napp.use(express.json());\n",[22,67721,67722,67736,67750,67764,67777,67790,67794,67799,67834,67838,67843,67869,67886,67895,67900,67910,67914,67918,67922,67935,67949],{"__ignoreMap":133},[137,67723,67724,67726,67729,67731,67734],{"class":139,"line":140},[137,67725,10287],{"class":143},[137,67727,67728],{"class":157}," express, { Request, Response } ",[137,67730,10954],{"class":143},[137,67732,67733],{"class":284}," \"express\"",[137,67735,3276],{"class":157},[137,67737,67738,67740,67743,67745,67748],{"class":139,"line":173},[137,67739,10287],{"class":143},[137,67741,67742],{"class":157}," cors ",[137,67744,10954],{"class":143},[137,67746,67747],{"class":284}," \"cors\"",[137,67749,3276],{"class":157},[137,67751,67752,67754,67757,67759,67762],{"class":139,"line":188},[137,67753,10287],{"class":143},[137,67755,67756],{"class":157}," { GoogleGenerativeAI } ",[137,67758,10954],{"class":143},[137,67760,67761],{"class":284}," \"@google\u002Fgenerative-ai\"",[137,67763,3276],{"class":157},[137,67765,67766,67768,67771,67773,67775],{"class":139,"line":269},[137,67767,10287],{"class":143},[137,67769,67770],{"class":157}," { readFileSync } ",[137,67772,10954],{"class":143},[137,67774,48454],{"class":284},[137,67776,3276],{"class":157},[137,67778,67779,67781,67784,67786,67788],{"class":139,"line":278},[137,67780,10287],{"class":143},[137,67782,67783],{"class":157}," { dirname, join } ",[137,67785,10954],{"class":143},[137,67787,48468],{"class":284},[137,67789,3276],{"class":157},[137,67791,67792],{"class":139,"line":291},[137,67793,516],{"emptyLinePlaceholder":515},[137,67795,67796],{"class":139,"line":297},[137,67797,67798],{"class":308},"\u002F\u002F Load A2UI schema\n",[137,67800,67801,67803,67806,67808,67810,67812,67814,67816,67818,67820,67822,67825,67828,67830,67832],{"class":139,"line":302},[137,67802,3077],{"class":143},[137,67804,67805],{"class":364}," A2UI_SCHEMA",[137,67807,151],{"class":143},[137,67809,17436],{"class":364},[137,67811,1017],{"class":157},[137,67813,17441],{"class":147},[137,67815,356],{"class":157},[137,67817,48585],{"class":147},[137,67819,356],{"class":157},[137,67821,8628],{"class":147},[137,67823,67824],{"class":157},"(__dirname, ",[137,67826,67827],{"class":284},"\"a2ui-schema.json\"",[137,67829,2214],{"class":157},[137,67831,48601],{"class":284},[137,67833,8614],{"class":157},[137,67835,67836],{"class":139,"line":662},[137,67837,516],{"emptyLinePlaceholder":515},[137,67839,67840],{"class":139,"line":667},[137,67841,67842],{"class":308},"\u002F\u002F Initialize Gemini\n",[137,67844,67845,67847,67850,67852,67854,67857,67860,67863,67865,67867],{"class":139,"line":786},[137,67846,3077],{"class":143},[137,67848,67849],{"class":364}," genAI",[137,67851,151],{"class":143},[137,67853,1426],{"class":143},[137,67855,67856],{"class":147}," GoogleGenerativeAI",[137,67858,67859],{"class":157},"(process.env.",[137,67861,67862],{"class":364},"GEMINI_API_KEY",[137,67864,24707],{"class":143},[137,67866,4607],{"class":284},[137,67868,1502],{"class":157},[137,67870,67871,67873,67876,67878,67881,67884],{"class":139,"line":798},[137,67872,3077],{"class":143},[137,67874,67875],{"class":364}," model",[137,67877,151],{"class":143},[137,67879,67880],{"class":157}," genAI.",[137,67882,67883],{"class":147},"getGenerativeModel",[137,67885,3175],{"class":157},[137,67887,67888,67890,67893],{"class":139,"line":803},[137,67889,57491],{"class":157},[137,67891,67892],{"class":284},"\"gemini-2.5-flash\"",[137,67894,1961],{"class":157},[137,67896,67897],{"class":139,"line":931},[137,67898,67899],{"class":157},"    generationConfig: {\n",[137,67901,67902,67905,67908],{"class":139,"line":1568},[137,67903,67904],{"class":157},"        responseMimeType: ",[137,67906,67907],{"class":284},"\"application\u002Fjson\"",[137,67909,1961],{"class":157},[137,67911,67912],{"class":139,"line":1573},[137,67913,775],{"class":157},[137,67915,67916],{"class":139,"line":1578},[137,67917,5422],{"class":157},[137,67919,67920],{"class":139,"line":1588},[137,67921,516],{"emptyLinePlaceholder":515},[137,67923,67924,67926,67928,67930,67933],{"class":139,"line":1601},[137,67925,3077],{"class":143},[137,67927,15064],{"class":364},[137,67929,151],{"class":143},[137,67931,67932],{"class":147}," express",[137,67934,924],{"class":157},[137,67936,67937,67939,67942,67944,67947],{"class":139,"line":3802},[137,67938,21014],{"class":157},[137,67940,67941],{"class":147},"use",[137,67943,356],{"class":157},[137,67945,67946],{"class":147},"cors",[137,67948,14173],{"class":157},[137,67950,67951,67953,67955,67958,67960],{"class":139,"line":3808},[137,67952,21014],{"class":157},[137,67954,67941],{"class":147},[137,67956,67957],{"class":157},"(express.",[137,67959,5157],{"class":147},[137,67961,14173],{"class":157},[27,67963,67964,67965,67968],{},"The key configuration is ",[22,67966,67967],{},"responseMimeType: \"application\u002Fjson\"",", which ensures Gemini returns valid JSON that can be parsed directly.",[123,67970,67972],{"id":67971},"the-system-prompt","The System Prompt",[27,67974,67975],{},"The system prompt is where the magic happens. It teaches Gemini:",[2569,67977,67978,67984,67990],{},[1006,67979,67980,67983],{},[42,67981,67982],{},"What components exist"," (the catalog)",[1006,67985,67986,67989],{},[42,67987,67988],{},"When to use each one"," (content patterns)",[1006,67991,67992,67994],{},[42,67993,66808],{}," (topic-based colours)",[27,67996,67997],{},"Here's a condensed version showing the key sections (for the full version, visit the repo linked to this article):",[128,67999,68001],{"className":50379,"code":68000,"language":29200,"meta":133,"style":133},"const SYSTEM_PROMPT = `You are an A2UI agent. Analyze article content \nand generate a UI layout using A2UI components WITH CUSTOM STYLING.\n\n## Available Components:\n\n1. **HeroSection** - Full-width hero with image, title, subtitle\n   - Use for: Articles with prominent images, feature articles\n   - Properties: title, subtitle, imageUrl, height, style\n\n2. **TextBlock** - Rich text content (supports markdown)\n   - Use for: Body paragraphs, introductions\n   - Properties: content, variant (body|lead|small), style\n\n3. **ImageGallery** - Grid of images with lightbox\n   - Use for: Photography articles, visual guides\n   - Properties: images [{url, caption}], columns (1-4), style\n\n4. **CodeBlock** - Syntax-highlighted code\n   - Use for: Technical articles, tutorials\n   - Properties: code, language, title, style\n\n5. **Callout** - Highlighted information box\n   - Use for: Tips, warnings, important notes\n   - Properties: content, type (info|warning|success|tip), style\n\n...more components\n\n## Content-Based Styling Guidelines:\n\nChoose styling based on the article's TOPIC:\n\n### Technology\u002FCode Content\n- Use cool colors: deep blues (#1e3a5f), teals (#0d9488)\n- Dark backgrounds with light text for code sections\n- Example: \"backgroundColor\": \"#0f172a\", \"textColor\": \"#e2e8f0\"\n\n### Nature\u002FOutdoor Content  \n- Use earthy colors: greens (#2d5016), browns, sky blues\n- Soft shadows, medium border radius\n- Example: \"gradient\": { \"from\": \"#2d5016\", \"to\": \"#6b8e23\" }\n\n### Food\u002FCooking Content\n- Use warm colors: oranges (#ea580c), reds, yellows\n- Inviting feel with soft shadows\n- Example: \"backgroundColor\": \"#fef3c7\", \"textColor\": \"#92400e\"\n\n...more styling guidelines\n\n## Response Format:\n\nReturn JSON with this structure:\n{\n  \"surfaceUpdate\": {\n    \"surfaceId\": \"article-view\",\n    \"components\": [\n      {\n        \"id\": \"unique-id\",\n        \"component\": {\n          \"ComponentName\": { ...properties, \"style\": {...} }\n        }\n      }\n    ]\n  }\n}\n`;\n",[22,68002,68003,68015,68020,68024,68029,68033,68038,68043,68048,68052,68057,68062,68067,68071,68076,68081,68086,68090,68095,68100,68105,68109,68114,68119,68124,68128,68133,68137,68142,68146,68151,68155,68160,68165,68170,68175,68179,68184,68189,68194,68199,68203,68208,68213,68218,68223,68227,68232,68236,68241,68245,68250,68254,68259,68264,68269,68274,68279,68284,68289,68293,68297,68302,68306,68310],{"__ignoreMap":133},[137,68004,68005,68007,68010,68012],{"class":139,"line":140},[137,68006,3077],{"class":143},[137,68008,68009],{"class":364}," SYSTEM_PROMPT",[137,68011,151],{"class":143},[137,68013,68014],{"class":284}," `You are an A2UI agent. Analyze article content \n",[137,68016,68017],{"class":139,"line":173},[137,68018,68019],{"class":284},"and generate a UI layout using A2UI components WITH CUSTOM STYLING.\n",[137,68021,68022],{"class":139,"line":188},[137,68023,516],{"emptyLinePlaceholder":515},[137,68025,68026],{"class":139,"line":269},[137,68027,68028],{"class":284},"## Available Components:\n",[137,68030,68031],{"class":139,"line":278},[137,68032,516],{"emptyLinePlaceholder":515},[137,68034,68035],{"class":139,"line":291},[137,68036,68037],{"class":284},"1. **HeroSection** - Full-width hero with image, title, subtitle\n",[137,68039,68040],{"class":139,"line":297},[137,68041,68042],{"class":284},"   - Use for: Articles with prominent images, feature articles\n",[137,68044,68045],{"class":139,"line":302},[137,68046,68047],{"class":284},"   - Properties: title, subtitle, imageUrl, height, style\n",[137,68049,68050],{"class":139,"line":662},[137,68051,516],{"emptyLinePlaceholder":515},[137,68053,68054],{"class":139,"line":667},[137,68055,68056],{"class":284},"2. **TextBlock** - Rich text content (supports markdown)\n",[137,68058,68059],{"class":139,"line":786},[137,68060,68061],{"class":284},"   - Use for: Body paragraphs, introductions\n",[137,68063,68064],{"class":139,"line":798},[137,68065,68066],{"class":284},"   - Properties: content, variant (body|lead|small), style\n",[137,68068,68069],{"class":139,"line":803},[137,68070,516],{"emptyLinePlaceholder":515},[137,68072,68073],{"class":139,"line":931},[137,68074,68075],{"class":284},"3. **ImageGallery** - Grid of images with lightbox\n",[137,68077,68078],{"class":139,"line":1568},[137,68079,68080],{"class":284},"   - Use for: Photography articles, visual guides\n",[137,68082,68083],{"class":139,"line":1573},[137,68084,68085],{"class":284},"   - Properties: images [{url, caption}], columns (1-4), style\n",[137,68087,68088],{"class":139,"line":1578},[137,68089,516],{"emptyLinePlaceholder":515},[137,68091,68092],{"class":139,"line":1588},[137,68093,68094],{"class":284},"4. **CodeBlock** - Syntax-highlighted code\n",[137,68096,68097],{"class":139,"line":1601},[137,68098,68099],{"class":284},"   - Use for: Technical articles, tutorials\n",[137,68101,68102],{"class":139,"line":3802},[137,68103,68104],{"class":284},"   - Properties: code, language, title, style\n",[137,68106,68107],{"class":139,"line":3808},[137,68108,516],{"emptyLinePlaceholder":515},[137,68110,68111],{"class":139,"line":3822},[137,68112,68113],{"class":284},"5. **Callout** - Highlighted information box\n",[137,68115,68116],{"class":139,"line":3827},[137,68117,68118],{"class":284},"   - Use for: Tips, warnings, important notes\n",[137,68120,68121],{"class":139,"line":3832},[137,68122,68123],{"class":284},"   - Properties: content, type (info|warning|success|tip), style\n",[137,68125,68126],{"class":139,"line":3840},[137,68127,516],{"emptyLinePlaceholder":515},[137,68129,68130],{"class":139,"line":3846},[137,68131,68132],{"class":284},"...more components\n",[137,68134,68135],{"class":139,"line":3861},[137,68136,516],{"emptyLinePlaceholder":515},[137,68138,68139],{"class":139,"line":3883},[137,68140,68141],{"class":284},"## Content-Based Styling Guidelines:\n",[137,68143,68144],{"class":139,"line":3896},[137,68145,516],{"emptyLinePlaceholder":515},[137,68147,68148],{"class":139,"line":3901},[137,68149,68150],{"class":284},"Choose styling based on the article's TOPIC:\n",[137,68152,68153],{"class":139,"line":3906},[137,68154,516],{"emptyLinePlaceholder":515},[137,68156,68157],{"class":139,"line":3911},[137,68158,68159],{"class":284},"### Technology\u002FCode Content\n",[137,68161,68162],{"class":139,"line":4666},[137,68163,68164],{"class":284},"- Use cool colors: deep blues (#1e3a5f), teals (#0d9488)\n",[137,68166,68167],{"class":139,"line":4672},[137,68168,68169],{"class":284},"- Dark backgrounds with light text for code sections\n",[137,68171,68172],{"class":139,"line":4680},[137,68173,68174],{"class":284},"- Example: \"backgroundColor\": \"#0f172a\", \"textColor\": \"#e2e8f0\"\n",[137,68176,68177],{"class":139,"line":4711},[137,68178,516],{"emptyLinePlaceholder":515},[137,68180,68181],{"class":139,"line":4716},[137,68182,68183],{"class":284},"### Nature\u002FOutdoor Content  \n",[137,68185,68186],{"class":139,"line":4721},[137,68187,68188],{"class":284},"- Use earthy colors: greens (#2d5016), browns, sky blues\n",[137,68190,68191],{"class":139,"line":4727},[137,68192,68193],{"class":284},"- Soft shadows, medium border radius\n",[137,68195,68196],{"class":139,"line":4732},[137,68197,68198],{"class":284},"- Example: \"gradient\": { \"from\": \"#2d5016\", \"to\": \"#6b8e23\" }\n",[137,68200,68201],{"class":139,"line":5006},[137,68202,516],{"emptyLinePlaceholder":515},[137,68204,68205],{"class":139,"line":5014},[137,68206,68207],{"class":284},"### Food\u002FCooking Content\n",[137,68209,68210],{"class":139,"line":14343},[137,68211,68212],{"class":284},"- Use warm colors: oranges (#ea580c), reds, yellows\n",[137,68214,68215],{"class":139,"line":24199},[137,68216,68217],{"class":284},"- Inviting feel with soft shadows\n",[137,68219,68220],{"class":139,"line":24773},[137,68221,68222],{"class":284},"- Example: \"backgroundColor\": \"#fef3c7\", \"textColor\": \"#92400e\"\n",[137,68224,68225],{"class":139,"line":24778},[137,68226,516],{"emptyLinePlaceholder":515},[137,68228,68229],{"class":139,"line":24783},[137,68230,68231],{"class":284},"...more styling guidelines\n",[137,68233,68234],{"class":139,"line":24793},[137,68235,516],{"emptyLinePlaceholder":515},[137,68237,68238],{"class":139,"line":24827},[137,68239,68240],{"class":284},"## Response Format:\n",[137,68242,68243],{"class":139,"line":24857},[137,68244,516],{"emptyLinePlaceholder":515},[137,68246,68247],{"class":139,"line":24862},[137,68248,68249],{"class":284},"Return JSON with this structure:\n",[137,68251,68252],{"class":139,"line":24867},[137,68253,15971],{"class":284},[137,68255,68256],{"class":139,"line":24884},[137,68257,68258],{"class":284},"  \"surfaceUpdate\": {\n",[137,68260,68261],{"class":139,"line":24892},[137,68262,68263],{"class":284},"    \"surfaceId\": \"article-view\",\n",[137,68265,68266],{"class":139,"line":24902},[137,68267,68268],{"class":284},"    \"components\": [\n",[137,68270,68271],{"class":139,"line":24925},[137,68272,68273],{"class":284},"      {\n",[137,68275,68276],{"class":139,"line":24933},[137,68277,68278],{"class":284},"        \"id\": \"unique-id\",\n",[137,68280,68281],{"class":139,"line":24941},[137,68282,68283],{"class":284},"        \"component\": {\n",[137,68285,68286],{"class":139,"line":24952},[137,68287,68288],{"class":284},"          \"ComponentName\": { ...properties, \"style\": {...} }\n",[137,68290,68291],{"class":139,"line":24960},[137,68292,1966],{"class":284},[137,68294,68295],{"class":139,"line":24982},[137,68296,4847],{"class":284},[137,68298,68299],{"class":139,"line":24989},[137,68300,68301],{"class":284},"    ]\n",[137,68303,68304],{"class":139,"line":24994},[137,68305,3462],{"class":284},[137,68307,68308],{"class":139,"line":24999},[137,68309,510],{"class":284},[137,68311,68312,68314],{"class":139,"line":25004},[137,68313,22056],{"class":284},[137,68315,3276],{"class":157},[123,68317,68319],{"id":68318},"the-generation-function","The Generation Function",[27,68321,68322],{},"This function sends the article content and metadata to Gemini:",[128,68324,68326],{"className":50379,"code":68325,"language":29200,"meta":133,"style":133},"async function generateA2UIComponents(articleContent: string, articleMeta: ArticleMeta): Promise\u003CA2UISurfaceUpdate> {\n    const prompt = `\n## Article Metadata:\n${JSON.stringify(articleMeta, null, 2)}\n\n## Article Content (Markdown):\n${articleContent}\n\n## Task:\nAnalyze this article and generate an A2UI component layout.\nConsider the content type, structure, and any special elements.\n\nGenerate the A2UI JSON response:\n`;\n\n    const result = await model.generateContent([{ text: SYSTEM_PROMPT }, { text: prompt }]);\n\n    const response = result.response.text();\n    return JSON.parse(response);\n}\n",[22,68327,68328,68369,68380,68385,68411,68415,68420,68428,68432,68437,68442,68447,68451,68456,68462,68466,68491,68495,68510,68523],{"__ignoreMap":133},[137,68329,68330,68332,68334,68337,68339,68342,68344,68346,68348,68351,68353,68356,68358,68360,68362,68364,68367],{"class":139,"line":140},[137,68331,15050],{"class":143},[137,68333,154],{"class":143},[137,68335,68336],{"class":147}," generateA2UIComponents",[137,68338,356],{"class":157},[137,68340,68341],{"class":161},"articleContent",[137,68343,894],{"class":143},[137,68345,13630],{"class":364},[137,68347,164],{"class":157},[137,68349,68350],{"class":161},"articleMeta",[137,68352,894],{"class":143},[137,68354,68355],{"class":147}," ArticleMeta",[137,68357,14105],{"class":157},[137,68359,894],{"class":143},[137,68361,14116],{"class":147},[137,68363,4033],{"class":157},[137,68365,68366],{"class":147},"A2UISurfaceUpdate",[137,68368,14136],{"class":157},[137,68370,68371,68373,68376,68378],{"class":139,"line":173},[137,68372,4177],{"class":143},[137,68374,68375],{"class":364}," prompt",[137,68377,151],{"class":143},[137,68379,3778],{"class":284},[137,68381,68382],{"class":139,"line":188},[137,68383,68384],{"class":284},"## Article Metadata:\n",[137,68386,68387,68389,68391,68393,68395,68397,68399,68401,68403,68405,68407,68409],{"class":139,"line":269},[137,68388,49545],{"class":284},[137,68390,22554],{"class":364},[137,68392,1017],{"class":284},[137,68394,24816],{"class":147},[137,68396,356],{"class":284},[137,68398,68350],{"class":157},[137,68400,164],{"class":284},[137,68402,11700],{"class":364},[137,68404,164],{"class":284},[137,68406,10345],{"class":364},[137,68408,14105],{"class":284},[137,68410,510],{"class":284},[137,68412,68413],{"class":139,"line":278},[137,68414,516],{"emptyLinePlaceholder":515},[137,68416,68417],{"class":139,"line":291},[137,68418,68419],{"class":284},"## Article Content (Markdown):\n",[137,68421,68422,68424,68426],{"class":139,"line":297},[137,68423,49545],{"class":284},[137,68425,68341],{"class":157},[137,68427,510],{"class":284},[137,68429,68430],{"class":139,"line":302},[137,68431,516],{"emptyLinePlaceholder":515},[137,68433,68434],{"class":139,"line":662},[137,68435,68436],{"class":284},"## Task:\n",[137,68438,68439],{"class":139,"line":667},[137,68440,68441],{"class":284},"Analyze this article and generate an A2UI component layout.\n",[137,68443,68444],{"class":139,"line":786},[137,68445,68446],{"class":284},"Consider the content type, structure, and any special elements.\n",[137,68448,68449],{"class":139,"line":798},[137,68450,516],{"emptyLinePlaceholder":515},[137,68452,68453],{"class":139,"line":803},[137,68454,68455],{"class":284},"Generate the A2UI JSON response:\n",[137,68457,68458,68460],{"class":139,"line":931},[137,68459,22056],{"class":284},[137,68461,3276],{"class":157},[137,68463,68464],{"class":139,"line":1568},[137,68465,516],{"emptyLinePlaceholder":515},[137,68467,68468,68470,68472,68474,68476,68479,68482,68485,68488],{"class":139,"line":1573},[137,68469,4177],{"class":143},[137,68471,26939],{"class":364},[137,68473,151],{"class":143},[137,68475,15069],{"class":143},[137,68477,68478],{"class":157}," model.",[137,68480,68481],{"class":147},"generateContent",[137,68483,68484],{"class":157},"([{ text: ",[137,68486,68487],{"class":364},"SYSTEM_PROMPT",[137,68489,68490],{"class":157}," }, { text: prompt }]);\n",[137,68492,68493],{"class":139,"line":1578},[137,68494,516],{"emptyLinePlaceholder":515},[137,68496,68497,68499,68501,68503,68506,68508],{"class":139,"line":1588},[137,68498,4177],{"class":143},[137,68500,49787],{"class":364},[137,68502,151],{"class":143},[137,68504,68505],{"class":157}," result.response.",[137,68507,5189],{"class":147},[137,68509,924],{"class":157},[137,68511,68512,68514,68516,68518,68520],{"class":139,"line":1601},[137,68513,176],{"class":143},[137,68515,17436],{"class":364},[137,68517,1017],{"class":157},[137,68519,17441],{"class":147},[137,68521,68522],{"class":157},"(response);\n",[137,68524,68525],{"class":139,"line":3802},[137,68526,510],{"class":157},[27,68528,68529],{},"Pass both the system prompt (the rules) and the user prompt (the actual article) to Gemini. The model analyses the content - detecting code blocks, images, and quotes - and returns structured A2UI JSON.",[123,68531,68533],{"id":68532},"the-api-endpoint","The API Endpoint",[27,68535,68536],{},"Finally, here's the endpoint that clients call:",[128,68538,68540],{"className":50379,"code":68539,"language":29200,"meta":133,"style":133},"app.post(\"\u002Fapi\u002Fa2ui\u002Frender\", async (req: Request, res: Response) => {\n    const { content, meta } = req.body;\n\n    if (!content) {\n        res.status(400).json({ error: \"Content is required\" });\n        return;\n    }\n\n    try {\n        console.log(`[A2UI Agent] Processing: ${meta?.title || \"Untitled\"}`);\n        const a2uiResponse = await generateA2UIComponents(content, meta || {});\n        res.json(a2uiResponse);\n    } catch (error) {\n        res.status(500).json({ error: \"Failed to generate UI components\" });\n    }\n});\n\nconst PORT = process.env.PORT || 3001;\napp.listen(PORT, () => {\n    console.log(`[A2UI Agent] Server running on http:\u002F\u002Flocalhost:${PORT}`);\n});\n",[22,68541,68542,68580,68599,68603,68614,68636,68642,68646,68650,68656,68681,68701,68710,68718,68739,68743,68747,68751,68772,68788,68805],{"__ignoreMap":133},[137,68543,68544,68546,68548,68550,68553,68555,68557,68559,68561,68563,68566,68568,68570,68572,68574,68576,68578],{"class":139,"line":140},[137,68545,21014],{"class":157},[137,68547,12],{"class":147},[137,68549,356],{"class":157},[137,68551,68552],{"class":284},"\"\u002Fapi\u002Fa2ui\u002Frender\"",[137,68554,164],{"class":157},[137,68556,15050],{"class":143},[137,68558,158],{"class":157},[137,68560,9133],{"class":161},[137,68562,894],{"class":143},[137,68564,68565],{"class":147}," Request",[137,68567,164],{"class":157},[137,68569,9138],{"class":161},[137,68571,894],{"class":143},[137,68573,50285],{"class":147},[137,68575,219],{"class":157},[137,68577,222],{"class":143},[137,68579,256],{"class":157},[137,68581,68582,68584,68586,68588,68590,68592,68594,68596],{"class":139,"line":173},[137,68583,4177],{"class":143},[137,68585,8906],{"class":157},[137,68587,29728],{"class":364},[137,68589,164],{"class":157},[137,68591,23508],{"class":364},[137,68593,8911],{"class":157},[137,68595,253],{"class":143},[137,68597,68598],{"class":157}," req.body;\n",[137,68600,68601],{"class":139,"line":188},[137,68602,516],{"emptyLinePlaceholder":515},[137,68604,68605,68607,68609,68611],{"class":139,"line":269},[137,68606,24696],{"class":143},[137,68608,158],{"class":157},[137,68610,17393],{"class":143},[137,68612,68613],{"class":157},"content) {\n",[137,68615,68616,68618,68620,68622,68625,68627,68629,68631,68634],{"class":139,"line":278},[137,68617,33014],{"class":157},[137,68619,9166],{"class":147},[137,68621,356],{"class":157},[137,68623,68624],{"class":364},"400",[137,68626,4409],{"class":157},[137,68628,5157],{"class":147},[137,68630,50240],{"class":157},[137,68632,68633],{"class":284},"\"Content is required\"",[137,68635,4168],{"class":157},[137,68637,68638,68640],{"class":139,"line":291},[137,68639,5472],{"class":143},[137,68641,3276],{"class":157},[137,68643,68644],{"class":139,"line":297},[137,68645,294],{"class":157},[137,68647,68648],{"class":139,"line":302},[137,68649,516],{"emptyLinePlaceholder":515},[137,68651,68652,68654],{"class":139,"line":662},[137,68653,25035],{"class":143},[137,68655,256],{"class":157},[137,68657,68658,68660,68662,68664,68667,68669,68672,68674,68676,68679],{"class":139,"line":667},[137,68659,350],{"class":157},[137,68661,353],{"class":147},[137,68663,356],{"class":157},[137,68665,68666],{"class":284},"`[A2UI Agent] Processing: ${",[137,68668,23508],{"class":157},[137,68670,68671],{"class":284},"?.",[137,68673,25683],{"class":157},[137,68675,24707],{"class":143},[137,68677,68678],{"class":284}," \"Untitled\"}`",[137,68680,1502],{"class":157},[137,68682,68683,68685,68688,68690,68692,68694,68697,68699],{"class":139,"line":786},[137,68684,3008],{"class":143},[137,68686,68687],{"class":364}," a2uiResponse",[137,68689,151],{"class":143},[137,68691,15069],{"class":143},[137,68693,68336],{"class":147},[137,68695,68696],{"class":157},"(content, meta ",[137,68698,50706],{"class":143},[137,68700,24638],{"class":157},[137,68702,68703,68705,68707],{"class":139,"line":798},[137,68704,33014],{"class":157},[137,68706,5157],{"class":147},[137,68708,68709],{"class":157},"(a2uiResponse);\n",[137,68711,68712,68714,68716],{"class":139,"line":803},[137,68713,24944],{"class":157},[137,68715,2807],{"class":143},[137,68717,15734],{"class":157},[137,68719,68720,68722,68724,68726,68728,68730,68732,68734,68737],{"class":139,"line":931},[137,68721,33014],{"class":157},[137,68723,9166],{"class":147},[137,68725,356],{"class":157},[137,68727,60661],{"class":364},[137,68729,4409],{"class":157},[137,68731,5157],{"class":147},[137,68733,50240],{"class":157},[137,68735,68736],{"class":284},"\"Failed to generate UI components\"",[137,68738,4168],{"class":157},[137,68740,68741],{"class":139,"line":1568},[137,68742,294],{"class":157},[137,68744,68745],{"class":139,"line":1573},[137,68746,5422],{"class":157},[137,68748,68749],{"class":139,"line":1578},[137,68750,516],{"emptyLinePlaceholder":515},[137,68752,68753,68755,68758,68760,68762,68765,68767,68770],{"class":139,"line":1588},[137,68754,3077],{"class":143},[137,68756,68757],{"class":364}," PORT",[137,68759,151],{"class":143},[137,68761,13635],{"class":157},[137,68763,68764],{"class":364},"PORT",[137,68766,24707],{"class":143},[137,68768,68769],{"class":364}," 3001",[137,68771,3276],{"class":157},[137,68773,68774,68776,68778,68780,68782,68784,68786],{"class":139,"line":1601},[137,68775,21014],{"class":157},[137,68777,15106],{"class":147},[137,68779,356],{"class":157},[137,68781,68764],{"class":364},[137,68783,4420],{"class":157},[137,68785,222],{"class":143},[137,68787,256],{"class":157},[137,68789,68790,68792,68794,68796,68799,68801,68803],{"class":139,"line":3802},[137,68791,493],{"class":157},[137,68793,353],{"class":147},[137,68795,356],{"class":157},[137,68797,68798],{"class":284},"`[A2UI Agent] Server running on http:\u002F\u002Flocalhost:${",[137,68800,68764],{"class":364},[137,68802,4706],{"class":284},[137,68804,1502],{"class":157},[137,68806,68807],{"class":139,"line":3808},[137,68808,5422],{"class":157},[27,68810,68811,68812,68815],{},"The client sends ",[22,68813,68814],{},"{ content: \"markdown...\", meta: { title, author, heroImage, ... } }"," and receives the full A2UI surface update with styled components.",[104,68817,68819],{"id":68818},"create-the-a2ui-client-service",[42,68820,68821],{},"Create the A2UI Client Service",[27,68823,68824,68825,68828],{},"The client service (",[22,68826,68827],{},"src\u002Fservices\u002Fa2ui-client.ts",") is a thin wrapper that sends article content to our agent and receives the A2UI component descriptions.",[128,68830,68832],{"className":50379,"code":68831,"language":29200,"meta":133,"style":133},"import type { A2UISurfaceUpdate, ArticleMeta } from \"..\u002Ftypes.js\";\n\nconst API_BASE = \"http:\u002F\u002Flocalhost:3001\u002Fapi\u002Fa2ui\";\n\nexport class A2UIClient {\n    \u002F**\n     * Send content to the A2UI agent and get back component descriptions\n     *\u002F\n    async render(content: string, meta: ArticleMeta): Promise\u003CA2UISurfaceUpdate> {\n        try {\n            const response = await fetch(`${API_BASE}\u002Frender`, {\n                method: \"POST\",\n                headers: { \"Content-Type\": \"application\u002Fjson\" },\n                body: JSON.stringify({ content, meta }),\n            });\n\n            if (!response.ok) {\n                throw new Error(\"Failed to get A2UI response\");\n            }\n\n            return await response.json();\n        } catch (error) {\n            console.error(\"A2UI Client Error:\", error);\n            \u002F\u002F Return a fallback UI if the agent fails\n            return this.getFallbackUI(content, meta);\n        }\n    }\n\n    \u002F**\n     * Fallback UI when the agent is unavailable\n     *\u002F\n    private getFallbackUI(content: string, meta: ArticleMeta): A2UISurfaceUpdate {\n        return {\n            surfaceUpdate: {\n                surfaceId: \"article-view\",\n                components: [\n                    {\n                        id: \"title-1\",\n                        component: {\n                            HeroSection: { title: meta.title, imageUrl: meta.heroImage },\n                        },\n                    },\n                    {\n                        id: \"content-1\",\n                        component: {\n                            TextBlock: { content: content, variant: \"body\" },\n                        },\n                    },\n                ],\n            },\n        };\n    }\n}\n\nexport const a2uiClient = new A2UIClient();\n",[22,68833,68834,68850,68854,68868,68872,68883,68888,68893,68898,68933,68939,68964,68974,68988,69002,69006,69010,69021,69036,69040,69044,69056,69064,69077,69082,69096,69100,69104,69108,69112,69117,69121,69153,69159,69164,69174,69179,69184,69194,69199,69204,69209,69213,69217,69226,69230,69240,69244,69248,69253,69257,69261,69265,69269,69273],{"__ignoreMap":133},[137,68835,68836,68838,68840,68843,68845,68848],{"class":139,"line":140},[137,68837,10287],{"class":143},[137,68839,25639],{"class":143},[137,68841,68842],{"class":157}," { A2UISurfaceUpdate, ArticleMeta } ",[137,68844,10954],{"class":143},[137,68846,68847],{"class":284}," \"..\u002Ftypes.js\"",[137,68849,3276],{"class":157},[137,68851,68852],{"class":139,"line":173},[137,68853,516],{"emptyLinePlaceholder":515},[137,68855,68856,68858,68861,68863,68866],{"class":139,"line":188},[137,68857,3077],{"class":143},[137,68859,68860],{"class":364}," API_BASE",[137,68862,151],{"class":143},[137,68864,68865],{"class":284}," \"http:\u002F\u002Flocalhost:3001\u002Fapi\u002Fa2ui\"",[137,68867,3276],{"class":157},[137,68869,68870],{"class":139,"line":269},[137,68871,516],{"emptyLinePlaceholder":515},[137,68873,68874,68876,68878,68881],{"class":139,"line":278},[137,68875,13456],{"class":143},[137,68877,7832],{"class":143},[137,68879,68880],{"class":147}," A2UIClient",[137,68882,256],{"class":157},[137,68884,68885],{"class":139,"line":291},[137,68886,68887],{"class":308},"    \u002F**\n",[137,68889,68890],{"class":139,"line":297},[137,68891,68892],{"class":308},"     * Send content to the A2UI agent and get back component descriptions\n",[137,68894,68895],{"class":139,"line":302},[137,68896,68897],{"class":308},"     *\u002F\n",[137,68899,68900,68902,68905,68907,68909,68911,68913,68915,68917,68919,68921,68923,68925,68927,68929,68931],{"class":139,"line":662},[137,68901,15546],{"class":143},[137,68903,68904],{"class":147}," render",[137,68906,356],{"class":157},[137,68908,29728],{"class":161},[137,68910,894],{"class":143},[137,68912,13630],{"class":364},[137,68914,164],{"class":157},[137,68916,23508],{"class":161},[137,68918,894],{"class":143},[137,68920,68355],{"class":147},[137,68922,14105],{"class":157},[137,68924,894],{"class":143},[137,68926,14116],{"class":147},[137,68928,4033],{"class":157},[137,68930,68366],{"class":147},[137,68932,14136],{"class":157},[137,68934,68935,68937],{"class":139,"line":667},[137,68936,15697],{"class":143},[137,68938,256],{"class":157},[137,68940,68941,68943,68945,68947,68949,68952,68954,68956,68959,68962],{"class":139,"line":786},[137,68942,5772],{"class":143},[137,68944,49787],{"class":364},[137,68946,151],{"class":143},[137,68948,15069],{"class":143},[137,68950,68951],{"class":147}," fetch",[137,68953,356],{"class":157},[137,68955,18820],{"class":284},[137,68957,68958],{"class":364},"API_BASE",[137,68960,68961],{"class":284},"}\u002Frender`",[137,68963,5396],{"class":157},[137,68965,68966,68969,68972],{"class":139,"line":798},[137,68967,68968],{"class":157},"                method: ",[137,68970,68971],{"class":284},"\"POST\"",[137,68973,1961],{"class":157},[137,68975,68976,68979,68982,68984,68986],{"class":139,"line":803},[137,68977,68978],{"class":157},"                headers: { ",[137,68980,68981],{"class":284},"\"Content-Type\"",[137,68983,726],{"class":157},[137,68985,67907],{"class":284},[137,68987,31293],{"class":157},[137,68989,68990,68993,68995,68997,68999],{"class":139,"line":931},[137,68991,68992],{"class":157},"                body: ",[137,68994,22554],{"class":364},[137,68996,1017],{"class":157},[137,68998,24816],{"class":147},[137,69000,69001],{"class":157},"({ content, meta }),\n",[137,69003,69004],{"class":139,"line":1568},[137,69005,14336],{"class":157},[137,69007,69008],{"class":139,"line":1573},[137,69009,516],{"emptyLinePlaceholder":515},[137,69011,69012,69014,69016,69018],{"class":139,"line":1578},[137,69013,5747],{"class":143},[137,69015,158],{"class":157},[137,69017,17393],{"class":143},[137,69019,69020],{"class":157},"response.ok) {\n",[137,69022,69023,69025,69027,69029,69031,69034],{"class":139,"line":1588},[137,69024,14324],{"class":143},[137,69026,1426],{"class":143},[137,69028,40520],{"class":147},[137,69030,356],{"class":157},[137,69032,69033],{"class":284},"\"Failed to get A2UI response\"",[137,69035,1502],{"class":157},[137,69037,69038],{"class":139,"line":1601},[137,69039,760],{"class":157},[137,69041,69042],{"class":139,"line":3802},[137,69043,516],{"emptyLinePlaceholder":515},[137,69045,69046,69048,69050,69052,69054],{"class":139,"line":3808},[137,69047,4683],{"class":143},[137,69049,15069],{"class":143},[137,69051,57802],{"class":157},[137,69053,5157],{"class":147},[137,69055,924],{"class":157},[137,69057,69058,69060,69062],{"class":139,"line":3822},[137,69059,15729],{"class":157},[137,69061,2807],{"class":143},[137,69063,15734],{"class":157},[137,69065,69066,69068,69070,69072,69075],{"class":139,"line":3827},[137,69067,1493],{"class":157},[137,69069,2812],{"class":147},[137,69071,356],{"class":157},[137,69073,69074],{"class":284},"\"A2UI Client Error:\"",[137,69076,17836],{"class":157},[137,69078,69079],{"class":139,"line":3832},[137,69080,69081],{"class":308},"            \u002F\u002F Return a fallback UI if the agent fails\n",[137,69083,69084,69086,69088,69090,69093],{"class":139,"line":3840},[137,69085,4683],{"class":143},[137,69087,365],{"class":364},[137,69089,1017],{"class":157},[137,69091,69092],{"class":147},"getFallbackUI",[137,69094,69095],{"class":157},"(content, meta);\n",[137,69097,69098],{"class":139,"line":3846},[137,69099,1966],{"class":157},[137,69101,69102],{"class":139,"line":3861},[137,69103,294],{"class":157},[137,69105,69106],{"class":139,"line":3883},[137,69107,516],{"emptyLinePlaceholder":515},[137,69109,69110],{"class":139,"line":3896},[137,69111,68887],{"class":308},[137,69113,69114],{"class":139,"line":3901},[137,69115,69116],{"class":308},"     * Fallback UI when the agent is unavailable\n",[137,69118,69119],{"class":139,"line":3906},[137,69120,68897],{"class":308},[137,69122,69123,69125,69128,69130,69132,69134,69136,69138,69140,69142,69144,69146,69148,69151],{"class":139,"line":3911},[137,69124,23393],{"class":143},[137,69126,69127],{"class":147}," getFallbackUI",[137,69129,356],{"class":157},[137,69131,29728],{"class":161},[137,69133,894],{"class":143},[137,69135,13630],{"class":364},[137,69137,164],{"class":157},[137,69139,23508],{"class":161},[137,69141,894],{"class":143},[137,69143,68355],{"class":147},[137,69145,14105],{"class":157},[137,69147,894],{"class":143},[137,69149,69150],{"class":147}," A2UISurfaceUpdate",[137,69152,256],{"class":157},[137,69154,69155,69157],{"class":139,"line":4666},[137,69156,5472],{"class":143},[137,69158,256],{"class":157},[137,69160,69161],{"class":139,"line":4672},[137,69162,69163],{"class":157},"            surfaceUpdate: {\n",[137,69165,69166,69169,69172],{"class":139,"line":4680},[137,69167,69168],{"class":157},"                surfaceId: ",[137,69170,69171],{"class":284},"\"article-view\"",[137,69173,1961],{"class":157},[137,69175,69176],{"class":139,"line":4711},[137,69177,69178],{"class":157},"                components: [\n",[137,69180,69181],{"class":139,"line":4716},[137,69182,69183],{"class":157},"                    {\n",[137,69185,69186,69189,69192],{"class":139,"line":4721},[137,69187,69188],{"class":157},"                        id: ",[137,69190,69191],{"class":284},"\"title-1\"",[137,69193,1961],{"class":157},[137,69195,69196],{"class":139,"line":4727},[137,69197,69198],{"class":157},"                        component: {\n",[137,69200,69201],{"class":139,"line":4732},[137,69202,69203],{"class":157},"                            HeroSection: { title: meta.title, imageUrl: meta.heroImage },\n",[137,69205,69206],{"class":139,"line":5006},[137,69207,69208],{"class":157},"                        },\n",[137,69210,69211],{"class":139,"line":5014},[137,69212,36372],{"class":157},[137,69214,69215],{"class":139,"line":14343},[137,69216,69183],{"class":157},[137,69218,69219,69221,69224],{"class":139,"line":24199},[137,69220,69188],{"class":157},[137,69222,69223],{"class":284},"\"content-1\"",[137,69225,1961],{"class":157},[137,69227,69228],{"class":139,"line":24773},[137,69229,69198],{"class":157},[137,69231,69232,69235,69238],{"class":139,"line":24778},[137,69233,69234],{"class":157},"                            TextBlock: { content: content, variant: ",[137,69236,69237],{"class":284},"\"body\"",[137,69239,31293],{"class":157},[137,69241,69242],{"class":139,"line":24783},[137,69243,69208],{"class":157},[137,69245,69246],{"class":139,"line":24793},[137,69247,36372],{"class":157},[137,69249,69250],{"class":139,"line":24827},[137,69251,69252],{"class":157},"                ],\n",[137,69254,69255],{"class":139,"line":24857},[137,69256,14074],{"class":157},[137,69258,69259],{"class":139,"line":24862},[137,69260,1507],{"class":157},[137,69262,69263],{"class":139,"line":24867},[137,69264,294],{"class":157},[137,69266,69267],{"class":139,"line":24884},[137,69268,510],{"class":157},[137,69270,69271],{"class":139,"line":24892},[137,69272,516],{"emptyLinePlaceholder":515},[137,69274,69275,69277,69279,69282,69284,69286,69288],{"class":139,"line":24902},[137,69276,13456],{"class":143},[137,69278,20388],{"class":143},[137,69280,69281],{"class":364}," a2uiClient",[137,69283,151],{"class":143},[137,69285,1426],{"class":143},[137,69287,68880],{"class":147},[137,69289,924],{"class":157},[27,69291,69292],{},"The client handles three tasks:",[2569,69294,69295,69304,69310],{},[1006,69296,69297,69300,69301],{},[42,69298,69299],{},"Sends content to the agent"," - POSTs the markdown content and metadata to ",[22,69302,69303],{},"\u002Fapi\u002Fa2ui\u002Frender",[1006,69305,69306,69309],{},[42,69307,69308],{},"Returns the A2UI response"," - Receives component descriptions ready for rendering",[1006,69311,69312,69315],{},[42,69313,69314],{},"Provides a fallback"," - Renders a basic layout if the agent is unavailable",[27,69317,69318],{},"Usage is simple:",[128,69320,69322],{"className":50379,"code":69321,"language":29200,"meta":133,"style":133},"const a2uiResponse = await a2uiClient.render(article.content, article.meta);\n\u002F\u002F a2uiResponse.surfaceUpdate.components contains the UI description\n",[22,69323,69324,69343],{"__ignoreMap":133},[137,69325,69326,69328,69330,69332,69334,69337,69340],{"class":139,"line":140},[137,69327,3077],{"class":143},[137,69329,68687],{"class":364},[137,69331,151],{"class":143},[137,69333,15069],{"class":143},[137,69335,69336],{"class":157}," a2uiClient.",[137,69338,69339],{"class":147},"render",[137,69341,69342],{"class":157},"(article.content, article.meta);\n",[137,69344,69345],{"class":139,"line":173},[137,69346,69347],{"class":308},"\u002F\u002F a2uiResponse.surfaceUpdate.components contains the UI description\n",[104,69349,69351],{"id":69350},"build-the-a2ui-renderer-lit-web-components",[42,69352,69353],{},"Build the A2UI Renderer (Lit Web Components)",[27,69355,69356,69357,69360],{},"The renderer (",[22,69358,69359],{},"src\u002Fcomponents\u002Fa2ui-renderer.ts",") bridges A2UI JSON and actual UI. It receives component descriptions from the agent and maps each to a Lit web component.",[123,69362,69364],{"id":69363},"the-core-renderer","The Core Renderer",[128,69366,69368],{"className":50379,"code":69367,"language":29200,"meta":133,"style":133},"import { LitElement, html, css, TemplateResult } from \"lit\";\nimport { customElement, property } from \"lit\u002Fdecorators.js\";\nimport type { A2UIComponent, A2UIComponentType } from \"..\u002Ftypes.js\";\n\n\u002F\u002F Import all A2UI component renderers\nimport \".\u002Fa2ui-hero.js\";\nimport \".\u002Fa2ui-text-block.js\";\nimport \".\u002Fa2ui-image-gallery.js\";\nimport \".\u002Fa2ui-code-block.js\";\nimport \".\u002Fa2ui-callout.js\";\nimport \".\u002Fa2ui-quote.js\";\n\u002F\u002F ...more components\n\n@customElement(\"a2ui-renderer\")\nexport class A2UIRenderer extends LitElement {\n    @property({ type: Array })\n    components: A2UIComponent[] = [];\n\n    \u002F\u002F Store components by ID for layout references\n    private componentMap = new Map\u003Cstring, A2UIComponent>();\n\n    render() {\n        \u002F\u002F Build component map for Row\u002FColumn children lookups\n        this.componentMap.clear();\n        this.components.forEach((c) => this.componentMap.set(c.id, c));\n\n        return html` \u003Cdiv class=\"a2ui-surface\">${this.components.map((c) => this.renderComponent(c))}\u003C\u002Fdiv> `;\n    }\n}\n",[22,69369,69370,69384,69398,69413,69417,69422,69431,69440,69449,69458,69467,69476,69481,69485,69499,69515,69524,69539,69543,69548,69572,69576,69583,69588,69600,69626,69630,69676,69680],{"__ignoreMap":133},[137,69371,69372,69374,69377,69379,69382],{"class":139,"line":140},[137,69373,10287],{"class":143},[137,69375,69376],{"class":157}," { LitElement, html, css, TemplateResult } ",[137,69378,10954],{"class":143},[137,69380,69381],{"class":284}," \"lit\"",[137,69383,3276],{"class":157},[137,69385,69386,69388,69391,69393,69396],{"class":139,"line":173},[137,69387,10287],{"class":143},[137,69389,69390],{"class":157}," { customElement, property } ",[137,69392,10954],{"class":143},[137,69394,69395],{"class":284}," \"lit\u002Fdecorators.js\"",[137,69397,3276],{"class":157},[137,69399,69400,69402,69404,69407,69409,69411],{"class":139,"line":188},[137,69401,10287],{"class":143},[137,69403,25639],{"class":143},[137,69405,69406],{"class":157}," { A2UIComponent, A2UIComponentType } ",[137,69408,10954],{"class":143},[137,69410,68847],{"class":284},[137,69412,3276],{"class":157},[137,69414,69415],{"class":139,"line":269},[137,69416,516],{"emptyLinePlaceholder":515},[137,69418,69419],{"class":139,"line":278},[137,69420,69421],{"class":308},"\u002F\u002F Import all A2UI component renderers\n",[137,69423,69424,69426,69429],{"class":139,"line":291},[137,69425,10287],{"class":143},[137,69427,69428],{"class":284}," \".\u002Fa2ui-hero.js\"",[137,69430,3276],{"class":157},[137,69432,69433,69435,69438],{"class":139,"line":297},[137,69434,10287],{"class":143},[137,69436,69437],{"class":284}," \".\u002Fa2ui-text-block.js\"",[137,69439,3276],{"class":157},[137,69441,69442,69444,69447],{"class":139,"line":302},[137,69443,10287],{"class":143},[137,69445,69446],{"class":284}," \".\u002Fa2ui-image-gallery.js\"",[137,69448,3276],{"class":157},[137,69450,69451,69453,69456],{"class":139,"line":662},[137,69452,10287],{"class":143},[137,69454,69455],{"class":284}," \".\u002Fa2ui-code-block.js\"",[137,69457,3276],{"class":157},[137,69459,69460,69462,69465],{"class":139,"line":667},[137,69461,10287],{"class":143},[137,69463,69464],{"class":284}," \".\u002Fa2ui-callout.js\"",[137,69466,3276],{"class":157},[137,69468,69469,69471,69474],{"class":139,"line":786},[137,69470,10287],{"class":143},[137,69472,69473],{"class":284}," \".\u002Fa2ui-quote.js\"",[137,69475,3276],{"class":157},[137,69477,69478],{"class":139,"line":798},[137,69479,69480],{"class":308},"\u002F\u002F ...more components\n",[137,69482,69483],{"class":139,"line":803},[137,69484,516],{"emptyLinePlaceholder":515},[137,69486,69487,69489,69492,69494,69497],{"class":139,"line":931},[137,69488,13382],{"class":157},[137,69490,69491],{"class":147},"customElement",[137,69493,356],{"class":157},[137,69495,69496],{"class":284},"\"a2ui-renderer\"",[137,69498,3155],{"class":157},[137,69500,69501,69503,69505,69508,69510,69513],{"class":139,"line":1568},[137,69502,13456],{"class":143},[137,69504,7832],{"class":143},[137,69506,69507],{"class":147}," A2UIRenderer",[137,69509,3641],{"class":143},[137,69511,69512],{"class":147}," LitElement",[137,69514,256],{"class":157},[137,69516,69517,69519,69521],{"class":139,"line":1573},[137,69518,14664],{"class":157},[137,69520,35259],{"class":147},[137,69522,69523],{"class":157},"({ type: Array })\n",[137,69525,69526,69528,69530,69533,69535,69537],{"class":139,"line":1578},[137,69527,51443],{"class":161},[137,69529,894],{"class":143},[137,69531,69532],{"class":147}," A2UIComponent",[137,69534,58220],{"class":157},[137,69536,253],{"class":143},[137,69538,8556],{"class":157},[137,69540,69541],{"class":139,"line":1588},[137,69542,516],{"emptyLinePlaceholder":515},[137,69544,69545],{"class":139,"line":1601},[137,69546,69547],{"class":308},"    \u002F\u002F Store components by ID for layout references\n",[137,69549,69550,69552,69555,69557,69559,69561,69563,69565,69567,69570],{"class":139,"line":3802},[137,69551,23393],{"class":143},[137,69553,69554],{"class":161}," componentMap",[137,69556,151],{"class":143},[137,69558,1426],{"class":143},[137,69560,63196],{"class":147},[137,69562,4033],{"class":157},[137,69564,14158],{"class":364},[137,69566,164],{"class":157},[137,69568,69569],{"class":147},"A2UIComponent",[137,69571,33894],{"class":157},[137,69573,69574],{"class":139,"line":3808},[137,69575,516],{"emptyLinePlaceholder":515},[137,69577,69578,69581],{"class":139,"line":3822},[137,69579,69580],{"class":147},"    render",[137,69582,275],{"class":157},[137,69584,69585],{"class":139,"line":3827},[137,69586,69587],{"class":308},"        \u002F\u002F Build component map for Row\u002FColumn children lookups\n",[137,69589,69590,69592,69595,69598],{"class":139,"line":3832},[137,69591,3679],{"class":364},[137,69593,69594],{"class":157},".componentMap.",[137,69596,69597],{"class":147},"clear",[137,69599,924],{"class":157},[137,69601,69602,69604,69607,69609,69611,69613,69615,69617,69619,69621,69623],{"class":139,"line":3840},[137,69603,3679],{"class":364},[137,69605,69606],{"class":157},".components.",[137,69608,8564],{"class":147},[137,69610,2774],{"class":157},[137,69612,33940],{"class":161},[137,69614,219],{"class":157},[137,69616,222],{"class":143},[137,69618,365],{"class":364},[137,69620,69594],{"class":157},[137,69622,35223],{"class":147},[137,69624,69625],{"class":157},"(c.id, c));\n",[137,69627,69628],{"class":139,"line":3846},[137,69629,516],{"emptyLinePlaceholder":515},[137,69631,69632,69634,69636,69639,69641,69643,69645,69647,69649,69651,69653,69655,69657,69659,69661,69664,69666,69668,69671,69674],{"class":139,"line":3861},[137,69633,5472],{"class":143},[137,69635,25580],{"class":147},[137,69637,69638],{"class":284},"` \u003Cdiv class=\"a2ui-surface\">${",[137,69640,24],{"class":364},[137,69642,1017],{"class":284},[137,69644,30208],{"class":157},[137,69646,1017],{"class":284},[137,69648,37476],{"class":147},[137,69650,2774],{"class":284},[137,69652,33940],{"class":364},[137,69654,219],{"class":284},[137,69656,222],{"class":143},[137,69658,365],{"class":364},[137,69660,1017],{"class":284},[137,69662,69663],{"class":147},"renderComponent",[137,69665,356],{"class":284},[137,69667,33940],{"class":157},[137,69669,69670],{"class":284},"))",[137,69672,69673],{"class":284},"}\u003C\u002Fdiv> `",[137,69675,3276],{"class":157},[137,69677,69678],{"class":139,"line":3883},[137,69679,294],{"class":157},[137,69681,69682],{"class":139,"line":3896},[137,69683,510],{"class":157},[27,69685,69686,69687,69690,69691,114,69693,69695],{},"The renderer maintains a ",[22,69688,69689],{},"componentMap"," that stores components by ID. This is essential for layout components like ",[22,69692,67198],{},[22,69694,31408],{},", which reference child components by ID.",[123,69697,69699],{"id":69698},"mapping-components","Mapping Components",[27,69701,4737,69702,69704],{},[22,69703,69663],{}," method uses a simple switch statement to map each A2UI component type to its Lit component:",[128,69706,69708],{"className":50379,"code":69707,"language":29200,"meta":133,"style":133},"private renderComponent(component: A2UIComponent): TemplateResult {\n  const { component: comp } = component;\n\n  \u002F\u002F Get the component type (first key in the object)\n  const type = Object.keys(comp)[0] as keyof A2UIComponentType;\n  const props = comp[type];\n\n  switch (type) {\n    case \"HeroSection\":\n      return html`\u003Ca2ui-hero .props=${props}>\u003C\u002Fa2ui-hero>`;\n\n    case \"TextBlock\":\n      return html`\u003Ca2ui-text-block .props=${props}>\u003C\u002Fa2ui-text-block>`;\n\n    case \"ImageGallery\":\n      return html`\u003Ca2ui-image-gallery .props=${props}>\u003C\u002Fa2ui-image-gallery>`;\n\n    case \"CodeBlock\":\n      return html`\u003Ca2ui-code-block .props=${props}>\u003C\u002Fa2ui-code-block>`;\n\n    case \"Callout\":\n      return html`\u003Ca2ui-callout .props=${props}>\u003C\u002Fa2ui-callout>`;\n\n    case \"Row\":\n      return this.renderRow(props);\n\n    case \"Column\":\n      return this.renderColumn(props);\n\n    default:\n      return html`\u003Cdiv class=\"error\">Unknown component: ${type}\u003C\u002Fdiv>`;\n  }\n}\n",[22,69709,69710,69720,69742,69746,69751,69781,69792,69796,69804,69814,69830,69834,69843,69859,69863,69872,69888,69892,69901,69917,69921,69930,69946,69950,69959,69973,69977,69986,69999,70003,70010,70026,70030],{"__ignoreMap":133},[137,69711,69712,69715,69717],{"class":139,"line":140},[137,69713,69714],{"class":157},"private ",[137,69716,69663],{"class":147},[137,69718,69719],{"class":157},"(component: A2UIComponent): TemplateResult {\n",[137,69721,69722,69725,69727,69730,69732,69735,69737,69739],{"class":139,"line":173},[137,69723,69724],{"class":143},"  const",[137,69726,8906],{"class":157},[137,69728,69729],{"class":161},"component",[137,69731,726],{"class":157},[137,69733,69734],{"class":364},"comp",[137,69736,8911],{"class":157},[137,69738,253],{"class":143},[137,69740,69741],{"class":157}," component;\n",[137,69743,69744],{"class":139,"line":188},[137,69745,516],{"emptyLinePlaceholder":515},[137,69747,69748],{"class":139,"line":269},[137,69749,69750],{"class":308},"  \u002F\u002F Get the component type (first key in the object)\n",[137,69752,69753,69755,69757,69759,69762,69765,69768,69770,69772,69774,69776,69779],{"class":139,"line":278},[137,69754,69724],{"class":143},[137,69756,25639],{"class":364},[137,69758,151],{"class":143},[137,69760,69761],{"class":157}," Object.",[137,69763,69764],{"class":147},"keys",[137,69766,69767],{"class":157},"(comp)[",[137,69769,6044],{"class":364},[137,69771,5796],{"class":157},[137,69773,24431],{"class":143},[137,69775,20363],{"class":143},[137,69777,69778],{"class":147}," A2UIComponentType",[137,69780,3276],{"class":157},[137,69782,69783,69785,69787,69789],{"class":139,"line":291},[137,69784,69724],{"class":143},[137,69786,29857],{"class":364},[137,69788,151],{"class":143},[137,69790,69791],{"class":157}," comp[type];\n",[137,69793,69794],{"class":139,"line":297},[137,69795,516],{"emptyLinePlaceholder":515},[137,69797,69798,69801],{"class":139,"line":302},[137,69799,69800],{"class":143},"  switch",[137,69802,69803],{"class":157}," (type) {\n",[137,69805,69806,69809,69812],{"class":139,"line":662},[137,69807,69808],{"class":143},"    case",[137,69810,69811],{"class":284}," \"HeroSection\"",[137,69813,36830],{"class":157},[137,69815,69816,69818,69820,69823,69825,69828],{"class":139,"line":667},[137,69817,3408],{"class":143},[137,69819,25580],{"class":147},[137,69821,69822],{"class":284},"`\u003Ca2ui-hero .props=${",[137,69824,29277],{"class":157},[137,69826,69827],{"class":284},"}>\u003C\u002Fa2ui-hero>`",[137,69829,3276],{"class":157},[137,69831,69832],{"class":139,"line":786},[137,69833,516],{"emptyLinePlaceholder":515},[137,69835,69836,69838,69841],{"class":139,"line":798},[137,69837,69808],{"class":143},[137,69839,69840],{"class":284}," \"TextBlock\"",[137,69842,36830],{"class":157},[137,69844,69845,69847,69849,69852,69854,69857],{"class":139,"line":803},[137,69846,3408],{"class":143},[137,69848,25580],{"class":147},[137,69850,69851],{"class":284},"`\u003Ca2ui-text-block .props=${",[137,69853,29277],{"class":157},[137,69855,69856],{"class":284},"}>\u003C\u002Fa2ui-text-block>`",[137,69858,3276],{"class":157},[137,69860,69861],{"class":139,"line":931},[137,69862,516],{"emptyLinePlaceholder":515},[137,69864,69865,69867,69870],{"class":139,"line":1568},[137,69866,69808],{"class":143},[137,69868,69869],{"class":284}," \"ImageGallery\"",[137,69871,36830],{"class":157},[137,69873,69874,69876,69878,69881,69883,69886],{"class":139,"line":1573},[137,69875,3408],{"class":143},[137,69877,25580],{"class":147},[137,69879,69880],{"class":284},"`\u003Ca2ui-image-gallery .props=${",[137,69882,29277],{"class":157},[137,69884,69885],{"class":284},"}>\u003C\u002Fa2ui-image-gallery>`",[137,69887,3276],{"class":157},[137,69889,69890],{"class":139,"line":1578},[137,69891,516],{"emptyLinePlaceholder":515},[137,69893,69894,69896,69899],{"class":139,"line":1588},[137,69895,69808],{"class":143},[137,69897,69898],{"class":284}," \"CodeBlock\"",[137,69900,36830],{"class":157},[137,69902,69903,69905,69907,69910,69912,69915],{"class":139,"line":1601},[137,69904,3408],{"class":143},[137,69906,25580],{"class":147},[137,69908,69909],{"class":284},"`\u003Ca2ui-code-block .props=${",[137,69911,29277],{"class":157},[137,69913,69914],{"class":284},"}>\u003C\u002Fa2ui-code-block>`",[137,69916,3276],{"class":157},[137,69918,69919],{"class":139,"line":3802},[137,69920,516],{"emptyLinePlaceholder":515},[137,69922,69923,69925,69928],{"class":139,"line":3808},[137,69924,69808],{"class":143},[137,69926,69927],{"class":284}," \"Callout\"",[137,69929,36830],{"class":157},[137,69931,69932,69934,69936,69939,69941,69944],{"class":139,"line":3822},[137,69933,3408],{"class":143},[137,69935,25580],{"class":147},[137,69937,69938],{"class":284},"`\u003Ca2ui-callout .props=${",[137,69940,29277],{"class":157},[137,69942,69943],{"class":284},"}>\u003C\u002Fa2ui-callout>`",[137,69945,3276],{"class":157},[137,69947,69948],{"class":139,"line":3827},[137,69949,516],{"emptyLinePlaceholder":515},[137,69951,69952,69954,69957],{"class":139,"line":3832},[137,69953,69808],{"class":143},[137,69955,69956],{"class":284}," \"Row\"",[137,69958,36830],{"class":157},[137,69960,69961,69963,69965,69967,69970],{"class":139,"line":3840},[137,69962,3408],{"class":143},[137,69964,365],{"class":364},[137,69966,1017],{"class":157},[137,69968,69969],{"class":147},"renderRow",[137,69971,69972],{"class":157},"(props);\n",[137,69974,69975],{"class":139,"line":3846},[137,69976,516],{"emptyLinePlaceholder":515},[137,69978,69979,69981,69984],{"class":139,"line":3861},[137,69980,69808],{"class":143},[137,69982,69983],{"class":284}," \"Column\"",[137,69985,36830],{"class":157},[137,69987,69988,69990,69992,69994,69997],{"class":139,"line":3883},[137,69989,3408],{"class":143},[137,69991,365],{"class":364},[137,69993,1017],{"class":157},[137,69995,69996],{"class":147},"renderColumn",[137,69998,69972],{"class":157},[137,70000,70001],{"class":139,"line":3896},[137,70002,516],{"emptyLinePlaceholder":515},[137,70004,70005,70008],{"class":139,"line":3901},[137,70006,70007],{"class":143},"    default",[137,70009,36830],{"class":157},[137,70011,70012,70014,70016,70019,70021,70024],{"class":139,"line":3906},[137,70013,3408],{"class":143},[137,70015,25580],{"class":147},[137,70017,70018],{"class":284},"`\u003Cdiv class=\"error\">Unknown component: ${",[137,70020,20355],{"class":157},[137,70022,70023],{"class":284},"}\u003C\u002Fdiv>`",[137,70025,3276],{"class":157},[137,70027,70028],{"class":139,"line":3911},[137,70029,3462],{"class":157},[137,70031,70032],{"class":139,"line":4666},[137,70033,510],{"class":157},[27,70035,70036],{},"Each case passes the props from the A2UI JSON to the corresponding Lit component, which handles its own rendering and styling.",[123,70038,70040],{"id":70039},"layout-components","Layout Components",[27,70042,70043,114,70045,70047],{},[22,70044,67198],{},[22,70046,31408],{}," are special - they reference other components by ID:",[128,70049,70051],{"className":50379,"code":70050,"language":29200,"meta":133,"style":133},"private renderRow(props: { children: string[]; gap?: string }): TemplateResult {\n  \u002F\u002F Look up child components by their IDs\n  const children = props.children\n    .map((id) => this.componentMap.get(id))\n    .filter((c): c is A2UIComponent => c !== undefined);\n\n  return html`\n    \u003Cdiv class=\"a2ui-row\" style=\"gap: ${this.getGapSize(props.gap)}\">\n      ${children.map((c) => this.renderComponent(c))}\n    \u003C\u002Fdiv>\n  `;\n}\n\nprivate renderColumn(props: { children: string[]; gap?: string }): TemplateResult {\n  const children = props.children\n    .map((id) => this.componentMap.get(id))\n    .filter((c): c is A2UIComponent => c !== undefined);\n\n  return html`\n    \u003Cdiv class=\"a2ui-column\" style=\"gap: ${this.getGapSize(props.gap)}\">\n      ${children.map((c) => this.renderComponent(c))}\n    \u003C\u002Fdiv>\n  `;\n}\n",[22,70052,70053,70067,70072,70084,70107,70140,70144,70153,70179,70212,70216,70222,70226,70230,70242,70252,70274,70304,70308,70316,70339,70371,70375,70381],{"__ignoreMap":133},[137,70054,70055,70057,70059,70062,70064],{"class":139,"line":140},[137,70056,69714],{"class":157},[137,70058,69969],{"class":147},[137,70060,70061],{"class":157},"(props: { children: string[]; gap",[137,70063,35824],{"class":143},[137,70065,70066],{"class":157}," string }): TemplateResult {\n",[137,70068,70069],{"class":139,"line":173},[137,70070,70071],{"class":308},"  \u002F\u002F Look up child components by their IDs\n",[137,70073,70074,70076,70079,70081],{"class":139,"line":188},[137,70075,69724],{"class":143},[137,70077,70078],{"class":364}," children",[137,70080,151],{"class":143},[137,70082,70083],{"class":157}," props.children\n",[137,70085,70086,70088,70090,70092,70094,70096,70098,70100,70102,70104],{"class":139,"line":269},[137,70087,2748],{"class":157},[137,70089,37476],{"class":147},[137,70091,2774],{"class":157},[137,70093,31478],{"class":161},[137,70095,219],{"class":157},[137,70097,222],{"class":143},[137,70099,365],{"class":364},[137,70101,69594],{"class":157},[137,70103,14153],{"class":147},[137,70105,70106],{"class":157},"(id))\n",[137,70108,70109,70111,70114,70116,70118,70120,70122,70125,70127,70129,70131,70133,70135,70138],{"class":139,"line":278},[137,70110,2748],{"class":157},[137,70112,70113],{"class":147},"filter",[137,70115,2774],{"class":157},[137,70117,33940],{"class":161},[137,70119,14105],{"class":157},[137,70121,894],{"class":143},[137,70123,70124],{"class":161}," c",[137,70126,21984],{"class":143},[137,70128,69532],{"class":147},[137,70130,59759],{"class":143},[137,70132,33981],{"class":157},[137,70134,26215],{"class":143},[137,70136,70137],{"class":364}," undefined",[137,70139,1502],{"class":157},[137,70141,70142],{"class":139,"line":291},[137,70143,516],{"emptyLinePlaceholder":515},[137,70145,70146,70149,70151],{"class":139,"line":297},[137,70147,70148],{"class":143},"  return",[137,70150,25580],{"class":147},[137,70152,22062],{"class":284},[137,70154,70155,70158,70160,70162,70165,70167,70169,70171,70174,70176],{"class":139,"line":302},[137,70156,70157],{"class":284},"    \u003Cdiv class=\"a2ui-row\" style=\"gap: ${",[137,70159,24],{"class":364},[137,70161,1017],{"class":284},[137,70163,70164],{"class":147},"getGapSize",[137,70166,356],{"class":284},[137,70168,29277],{"class":157},[137,70170,1017],{"class":284},[137,70172,70173],{"class":157},"gap",[137,70175,14105],{"class":284},[137,70177,70178],{"class":284},"}\">\n",[137,70180,70181,70184,70186,70188,70190,70192,70194,70196,70198,70200,70202,70204,70206,70208,70210],{"class":139,"line":662},[137,70182,70183],{"class":284},"      ${",[137,70185,37244],{"class":157},[137,70187,1017],{"class":284},[137,70189,37476],{"class":147},[137,70191,2774],{"class":284},[137,70193,33940],{"class":364},[137,70195,219],{"class":284},[137,70197,222],{"class":143},[137,70199,365],{"class":364},[137,70201,1017],{"class":284},[137,70203,69663],{"class":147},[137,70205,356],{"class":284},[137,70207,33940],{"class":157},[137,70209,69670],{"class":284},[137,70211,510],{"class":284},[137,70213,70214],{"class":139,"line":667},[137,70215,21366],{"class":284},[137,70217,70218,70220],{"class":139,"line":786},[137,70219,57484],{"class":284},[137,70221,3276],{"class":157},[137,70223,70224],{"class":139,"line":798},[137,70225,510],{"class":157},[137,70227,70228],{"class":139,"line":803},[137,70229,516],{"emptyLinePlaceholder":515},[137,70231,70232,70234,70236,70238,70240],{"class":139,"line":931},[137,70233,69714],{"class":157},[137,70235,69996],{"class":147},[137,70237,70061],{"class":157},[137,70239,35824],{"class":143},[137,70241,70066],{"class":157},[137,70243,70244,70246,70248,70250],{"class":139,"line":1568},[137,70245,69724],{"class":143},[137,70247,70078],{"class":364},[137,70249,151],{"class":143},[137,70251,70083],{"class":157},[137,70253,70254,70256,70258,70260,70262,70264,70266,70268,70270,70272],{"class":139,"line":1573},[137,70255,2748],{"class":157},[137,70257,37476],{"class":147},[137,70259,2774],{"class":157},[137,70261,31478],{"class":161},[137,70263,219],{"class":157},[137,70265,222],{"class":143},[137,70267,365],{"class":364},[137,70269,69594],{"class":157},[137,70271,14153],{"class":147},[137,70273,70106],{"class":157},[137,70275,70276,70278,70280,70282,70284,70286,70288,70290,70292,70294,70296,70298,70300,70302],{"class":139,"line":1578},[137,70277,2748],{"class":157},[137,70279,70113],{"class":147},[137,70281,2774],{"class":157},[137,70283,33940],{"class":161},[137,70285,14105],{"class":157},[137,70287,894],{"class":143},[137,70289,70124],{"class":161},[137,70291,21984],{"class":143},[137,70293,69532],{"class":147},[137,70295,59759],{"class":143},[137,70297,33981],{"class":157},[137,70299,26215],{"class":143},[137,70301,70137],{"class":364},[137,70303,1502],{"class":157},[137,70305,70306],{"class":139,"line":1588},[137,70307,516],{"emptyLinePlaceholder":515},[137,70309,70310,70312,70314],{"class":139,"line":1601},[137,70311,70148],{"class":143},[137,70313,25580],{"class":147},[137,70315,22062],{"class":284},[137,70317,70318,70321,70323,70325,70327,70329,70331,70333,70335,70337],{"class":139,"line":3802},[137,70319,70320],{"class":284},"    \u003Cdiv class=\"a2ui-column\" style=\"gap: ${",[137,70322,24],{"class":364},[137,70324,1017],{"class":284},[137,70326,70164],{"class":147},[137,70328,356],{"class":284},[137,70330,29277],{"class":157},[137,70332,1017],{"class":284},[137,70334,70173],{"class":157},[137,70336,14105],{"class":284},[137,70338,70178],{"class":284},[137,70340,70341,70343,70345,70347,70349,70351,70353,70355,70357,70359,70361,70363,70365,70367,70369],{"class":139,"line":3808},[137,70342,70183],{"class":284},[137,70344,37244],{"class":157},[137,70346,1017],{"class":284},[137,70348,37476],{"class":147},[137,70350,2774],{"class":284},[137,70352,33940],{"class":364},[137,70354,219],{"class":284},[137,70356,222],{"class":143},[137,70358,365],{"class":364},[137,70360,1017],{"class":284},[137,70362,69663],{"class":147},[137,70364,356],{"class":284},[137,70366,33940],{"class":157},[137,70368,69670],{"class":284},[137,70370,510],{"class":284},[137,70372,70373],{"class":139,"line":3822},[137,70374,21366],{"class":284},[137,70376,70377,70379],{"class":139,"line":3827},[137,70378,57484],{"class":284},[137,70380,3276],{"class":157},[137,70382,70383],{"class":139,"line":3832},[137,70384,510],{"class":157},[27,70386,70387],{},"This adjacency-list pattern (referencing children by ID rather than nesting) is key to the A2UI protocol. It keeps the JSON structure flat and easier for LLMs to generate.",[123,70389,70391],{"id":70390},"usage","Usage",[27,70393,70394],{},"Using the renderer in the article view is straightforward:",[128,70396,70398],{"className":50379,"code":70397,"language":29200,"meta":133,"style":133},"import \".\u002Fa2ui-renderer.js\";\n\n\u002F\u002F After getting A2UI response from the agent\nconst components = a2uiResponse.surfaceUpdate.components;\n\n\u002F\u002F Render in template\nhtml`\u003Ca2ui-renderer .components=${components}>\u003C\u002Fa2ui-renderer>`;\n",[22,70399,70400,70409,70413,70418,70430,70434,70439],{"__ignoreMap":133},[137,70401,70402,70404,70407],{"class":139,"line":140},[137,70403,10287],{"class":143},[137,70405,70406],{"class":284}," \".\u002Fa2ui-renderer.js\"",[137,70408,3276],{"class":157},[137,70410,70411],{"class":139,"line":173},[137,70412,516],{"emptyLinePlaceholder":515},[137,70414,70415],{"class":139,"line":188},[137,70416,70417],{"class":308},"\u002F\u002F After getting A2UI response from the agent\n",[137,70419,70420,70422,70425,70427],{"class":139,"line":269},[137,70421,3077],{"class":143},[137,70423,70424],{"class":364}," components",[137,70426,151],{"class":143},[137,70428,70429],{"class":157}," a2uiResponse.surfaceUpdate.components;\n",[137,70431,70432],{"class":139,"line":278},[137,70433,516],{"emptyLinePlaceholder":515},[137,70435,70436],{"class":139,"line":291},[137,70437,70438],{"class":308},"\u002F\u002F Render in template\n",[137,70440,70441,70443,70446,70448,70451],{"class":139,"line":297},[137,70442,4026],{"class":147},[137,70444,70445],{"class":284},"`\u003Ca2ui-renderer .components=${",[137,70447,30208],{"class":157},[137,70449,70450],{"class":284},"}>\u003C\u002Fa2ui-renderer>`",[137,70452,3276],{"class":157},[27,70454,70455],{},"The renderer handles the rest - mapping types, resolving children, and rendering the final UI.",[104,70457,70459],{"id":70458},"create-individual-a2ui-components",[42,70460,70461],{},"Create Individual A2UI Components",[27,70463,70464,70465,70468],{},"Each A2UI component is a Lit web component that receives props from the renderer and handles its own styling. Here's the Hero component (",[22,70466,70467],{},"src\u002Fcomponents\u002Fa2ui-hero.ts","):",[128,70470,70472],{"className":50379,"code":70471,"language":29200,"meta":133,"style":133},"import { LitElement, html, css } from \"lit\";\nimport { customElement, property } from \"lit\u002Fdecorators.js\";\nimport { styleMap } from \"lit\u002Fdirectives\u002Fstyle-map.js\";\nimport type { HeroSectionProps } from \"..\u002Ftypes.js\";\nimport { stylePropsToObject } from \"..\u002Futils\u002Fstyle-utils.js\";\n\n@customElement(\"a2ui-hero\")\nexport class A2UIHero extends LitElement {\n    static styles = css`...`; \u002F\u002F Component-specific CSS\n\n    @property({ type: Object })\n    props: HeroSectionProps = { title: \"\" };\n\n    render() {\n        const { title, subtitle, imageUrl, overlay = true, height = \"medium\", style } = this.props;\n\n        \u002F\u002F Convert A2UI style props to CSS\n        const customStyles = stylePropsToObject(style);\n\n        return html`\n            \u003Cdiv class=\"hero ${height}\" style=${styleMap(customStyles)}>\n                ${imageUrl ? html`\u003Cimg class=\"hero-image\" src=\"${imageUrl}\" alt=\"${title}\" \u002F>` : null}\n                ${imageUrl && overlay ? html`\u003Cdiv class=\"hero-overlay\">\u003C\u002Fdiv>` : null}\n                \u003Cdiv class=\"hero-content\">\n                    \u003Ch1 class=\"hero-title\">${title}\u003C\u002Fh1>\n                    ${subtitle ? html`\u003Cp class=\"hero-subtitle\">${subtitle}\u003C\u002Fp>` : null}\n                \u003C\u002Fdiv>\n            \u003C\u002Fdiv>\n        `;\n    }\n}\n",[22,70473,70474,70487,70499,70513,70528,70542,70546,70559,70574,70595,70599,70608,70627,70631,70637,70687,70691,70696,70711,70715,70723,70746,70776,70800,70805,70815,70839,70844,70849,70855,70859],{"__ignoreMap":133},[137,70475,70476,70478,70481,70483,70485],{"class":139,"line":140},[137,70477,10287],{"class":143},[137,70479,70480],{"class":157}," { LitElement, html, css } ",[137,70482,10954],{"class":143},[137,70484,69381],{"class":284},[137,70486,3276],{"class":157},[137,70488,70489,70491,70493,70495,70497],{"class":139,"line":173},[137,70490,10287],{"class":143},[137,70492,69390],{"class":157},[137,70494,10954],{"class":143},[137,70496,69395],{"class":284},[137,70498,3276],{"class":157},[137,70500,70501,70503,70506,70508,70511],{"class":139,"line":188},[137,70502,10287],{"class":143},[137,70504,70505],{"class":157}," { styleMap } ",[137,70507,10954],{"class":143},[137,70509,70510],{"class":284}," \"lit\u002Fdirectives\u002Fstyle-map.js\"",[137,70512,3276],{"class":157},[137,70514,70515,70517,70519,70522,70524,70526],{"class":139,"line":269},[137,70516,10287],{"class":143},[137,70518,25639],{"class":143},[137,70520,70521],{"class":157}," { HeroSectionProps } ",[137,70523,10954],{"class":143},[137,70525,68847],{"class":284},[137,70527,3276],{"class":157},[137,70529,70530,70532,70535,70537,70540],{"class":139,"line":278},[137,70531,10287],{"class":143},[137,70533,70534],{"class":157}," { stylePropsToObject } ",[137,70536,10954],{"class":143},[137,70538,70539],{"class":284}," \"..\u002Futils\u002Fstyle-utils.js\"",[137,70541,3276],{"class":157},[137,70543,70544],{"class":139,"line":291},[137,70545,516],{"emptyLinePlaceholder":515},[137,70547,70548,70550,70552,70554,70557],{"class":139,"line":297},[137,70549,13382],{"class":157},[137,70551,69491],{"class":147},[137,70553,356],{"class":157},[137,70555,70556],{"class":284},"\"a2ui-hero\"",[137,70558,3155],{"class":157},[137,70560,70561,70563,70565,70568,70570,70572],{"class":139,"line":302},[137,70562,13456],{"class":143},[137,70564,7832],{"class":143},[137,70566,70567],{"class":147}," A2UIHero",[137,70569,3641],{"class":143},[137,70571,69512],{"class":147},[137,70573,256],{"class":157},[137,70575,70576,70579,70582,70584,70587,70590,70592],{"class":139,"line":662},[137,70577,70578],{"class":143},"    static",[137,70580,70581],{"class":161}," styles",[137,70583,151],{"class":143},[137,70585,70586],{"class":147}," css",[137,70588,70589],{"class":284},"`...`",[137,70591,2323],{"class":157},[137,70593,70594],{"class":308},"\u002F\u002F Component-specific CSS\n",[137,70596,70597],{"class":139,"line":667},[137,70598,516],{"emptyLinePlaceholder":515},[137,70600,70601,70603,70605],{"class":139,"line":786},[137,70602,14664],{"class":157},[137,70604,35259],{"class":147},[137,70606,70607],{"class":157},"({ type: Object })\n",[137,70609,70610,70613,70615,70618,70620,70623,70625],{"class":139,"line":798},[137,70611,70612],{"class":161},"    props",[137,70614,894],{"class":143},[137,70616,70617],{"class":147}," HeroSectionProps",[137,70619,151],{"class":143},[137,70621,70622],{"class":157}," { title: ",[137,70624,4535],{"class":284},[137,70626,32107],{"class":157},[137,70628,70629],{"class":139,"line":803},[137,70630,516],{"emptyLinePlaceholder":515},[137,70632,70633,70635],{"class":139,"line":931},[137,70634,69580],{"class":147},[137,70636,275],{"class":157},[137,70638,70639,70641,70643,70645,70647,70650,70652,70655,70657,70660,70662,70664,70666,70669,70671,70674,70676,70678,70680,70682,70684],{"class":139,"line":1568},[137,70640,3008],{"class":143},[137,70642,8906],{"class":157},[137,70644,25683],{"class":364},[137,70646,164],{"class":157},[137,70648,70649],{"class":364},"subtitle",[137,70651,164],{"class":157},[137,70653,70654],{"class":364},"imageUrl",[137,70656,164],{"class":157},[137,70658,70659],{"class":364},"overlay",[137,70661,151],{"class":143},[137,70663,14286],{"class":364},[137,70665,164],{"class":157},[137,70667,70668],{"class":364},"height",[137,70670,151],{"class":143},[137,70672,70673],{"class":284}," \"medium\"",[137,70675,164],{"class":157},[137,70677,2617],{"class":364},[137,70679,8911],{"class":157},[137,70681,253],{"class":143},[137,70683,365],{"class":364},[137,70685,70686],{"class":157},".props;\n",[137,70688,70689],{"class":139,"line":1573},[137,70690,516],{"emptyLinePlaceholder":515},[137,70692,70693],{"class":139,"line":1578},[137,70694,70695],{"class":308},"        \u002F\u002F Convert A2UI style props to CSS\n",[137,70697,70698,70700,70703,70705,70708],{"class":139,"line":1588},[137,70699,3008],{"class":143},[137,70701,70702],{"class":364}," customStyles",[137,70704,151],{"class":143},[137,70706,70707],{"class":147}," stylePropsToObject",[137,70709,70710],{"class":157},"(style);\n",[137,70712,70713],{"class":139,"line":1601},[137,70714,516],{"emptyLinePlaceholder":515},[137,70716,70717,70719,70721],{"class":139,"line":3802},[137,70718,5472],{"class":143},[137,70720,25580],{"class":147},[137,70722,22062],{"class":284},[137,70724,70725,70728,70730,70733,70736,70738,70741,70743],{"class":139,"line":3808},[137,70726,70727],{"class":284},"            \u003Cdiv class=\"hero ${",[137,70729,70668],{"class":157},[137,70731,70732],{"class":284},"}\" style=${",[137,70734,70735],{"class":147},"styleMap",[137,70737,356],{"class":284},[137,70739,70740],{"class":157},"customStyles",[137,70742,14105],{"class":284},[137,70744,70745],{"class":284},"}>\n",[137,70747,70748,70751,70753,70755,70757,70760,70762,70765,70767,70770,70772,70774],{"class":139,"line":3822},[137,70749,70750],{"class":284},"                ${",[137,70752,70654],{"class":157},[137,70754,26196],{"class":143},[137,70756,25580],{"class":147},[137,70758,70759],{"class":284},"`\u003Cimg class=\"hero-image\" src=\"${",[137,70761,70654],{"class":157},[137,70763,70764],{"class":284},"}\" alt=\"${",[137,70766,25683],{"class":157},[137,70768,70769],{"class":284},"}\" \u002F>`",[137,70771,26201],{"class":143},[137,70773,3417],{"class":364},[137,70775,510],{"class":284},[137,70777,70778,70780,70782,70784,70787,70789,70791,70794,70796,70798],{"class":139,"line":3827},[137,70779,70750],{"class":284},[137,70781,70654],{"class":157},[137,70783,35905],{"class":143},[137,70785,70786],{"class":157}," overlay",[137,70788,26196],{"class":143},[137,70790,25580],{"class":147},[137,70792,70793],{"class":284},"`\u003Cdiv class=\"hero-overlay\">\u003C\u002Fdiv>`",[137,70795,26201],{"class":143},[137,70797,3417],{"class":364},[137,70799,510],{"class":284},[137,70801,70802],{"class":139,"line":3832},[137,70803,70804],{"class":284},"                \u003Cdiv class=\"hero-content\">\n",[137,70806,70807,70810,70812],{"class":139,"line":3840},[137,70808,70809],{"class":284},"                    \u003Ch1 class=\"hero-title\">${",[137,70811,25683],{"class":157},[137,70813,70814],{"class":284},"}\u003C\u002Fh1>\n",[137,70816,70817,70820,70822,70824,70826,70829,70831,70833,70835,70837],{"class":139,"line":3846},[137,70818,70819],{"class":284},"                    ${",[137,70821,70649],{"class":157},[137,70823,26196],{"class":143},[137,70825,25580],{"class":147},[137,70827,70828],{"class":284},"`\u003Cp class=\"hero-subtitle\">${",[137,70830,70649],{"class":157},[137,70832,59875],{"class":284},[137,70834,26201],{"class":143},[137,70836,3417],{"class":364},[137,70838,510],{"class":284},[137,70840,70841],{"class":139,"line":3861},[137,70842,70843],{"class":284},"                \u003C\u002Fdiv>\n",[137,70845,70846],{"class":139,"line":3883},[137,70847,70848],{"class":284},"            \u003C\u002Fdiv>\n",[137,70850,70851,70853],{"class":139,"line":3896},[137,70852,36433],{"class":284},[137,70854,3276],{"class":157},[137,70856,70857],{"class":139,"line":3901},[137,70858,294],{"class":157},[137,70860,70861],{"class":139,"line":3906},[137,70862,510],{"class":157},[27,70864,70865],{},"The pattern is consistent across all components:",[2569,70867,70868,70876,70885,70895],{},[1006,70869,70870,57180,70873,70875],{},[42,70871,70872],{},"Receive props",[22,70874,29277],{}," property contains everything the agent specified",[1006,70877,70878,70881,70882,14105],{},[42,70879,70880],{},"Extract values"," - Destructure with sensible defaults (",[22,70883,70884],{},"height = \"medium\"",[1006,70886,70887,70890,70891,70894],{},[42,70888,70889],{},"Apply custom styles"," - Use ",[22,70892,70893],{},"stylePropsToObject()"," to convert A2UI style hints to CSS",[1006,70896,70897,70900],{},[42,70898,70899],{},"Render conditionally"," - Show elements only when data exists",[27,70902,4737,70903,70906,70907,3955,70910,70913],{},[22,70904,70905],{},"stylePropsToObject"," utility converts A2UI's semantic style properties (like ",[22,70908,70909],{},"borderRadius: \"large\"",[22,70911,70912],{},"shadow: \"medium\"",") into actual CSS values. This keeps the agent's output simple while giving the client full control over visual implementation.",[104,70915,70917],{"id":70916},"create-sample-articles",[42,70918,70919],{},"Create Sample Articles",[27,70921,70922,70923,70926],{},"Articles live as Markdown files in ",[22,70924,70925],{},"public\u002Farticles\u002F",". Each file has YAML frontmatter for metadata and standard Markdown for content.",[27,70928,70929,70930,70468],{},"Here's a condensed example (",[22,70931,70932],{},"public\u002Farticles\u002Fmountain-photography.md",[128,70934,70937],{"className":70935,"code":70936,"language":57972,"meta":133,"style":133},"language-markdown shiki shiki-themes github-light github-dark","---\ntitle: \"Mountain Photography Guide\"\nauthor: \"Emma Nakamura\"\ndate: \"2024-12-15\"\nheroImage: \"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1506905925346-21bda4d32df4?w=1200\"\ntags: [\"photography\", \"nature\", \"travel\"]\nimages:\n    - url: \"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1464822759023-fed622ff2c3b?w=800\"\n      caption: \"Dramatic peaks at golden hour\"\n    - url: \"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1519681393784-d120267933ba?w=800\"\n      caption: \"Starry night over snow-capped mountains\"\n    - url: \"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1486870591958-9b9d0d1dda99?w=800\"\n      caption: \"Misty morning in the Alps\"\n---\n\n# Mountain Photography Guide\n\nCapturing the majesty of mountains requires patience, preparation,\nand an understanding of light.\n\n## Essential Gear\n\n- **Wide-angle lens** (16-35mm) for sweeping landscapes\n- **Telephoto lens** (70-200mm) for distant peaks\n- **Sturdy tripod** for long exposures\n- **ND filters** for controlling light\n\n## Best Times to Shoot\n\n> \"The best light happens when most people are still sleeping.\"\n\nGolden hour (sunrise\u002Fsunset) provides warm, dramatic lighting...\n\n## Photo Gallery\n\n![Golden hour ridge](https:\u002F\u002Fimages.unsplash.com\u002F...)\n![Night sky over peaks](https:\u002F\u002Fimages.unsplash.com\u002F...)\n",[22,70938,70939,70944,70949,70954,70959,70964,70969,70974,70979,70984,70989,70994,70999,71004,71008,71012,71017,71021,71026,71031,71035,71040,71044,71049,71054,71059,71064,71068,71073,71077,71082,71086,71091,71095,71100,71104,71109],{"__ignoreMap":133},[137,70940,70941],{"class":139,"line":140},[137,70942,70943],{},"---\n",[137,70945,70946],{"class":139,"line":173},[137,70947,70948],{},"title: \"Mountain Photography Guide\"\n",[137,70950,70951],{"class":139,"line":188},[137,70952,70953],{},"author: \"Emma Nakamura\"\n",[137,70955,70956],{"class":139,"line":269},[137,70957,70958],{},"date: \"2024-12-15\"\n",[137,70960,70961],{"class":139,"line":278},[137,70962,70963],{},"heroImage: \"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1506905925346-21bda4d32df4?w=1200\"\n",[137,70965,70966],{"class":139,"line":291},[137,70967,70968],{},"tags: [\"photography\", \"nature\", \"travel\"]\n",[137,70970,70971],{"class":139,"line":297},[137,70972,70973],{},"images:\n",[137,70975,70976],{"class":139,"line":302},[137,70977,70978],{},"    - url: \"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1464822759023-fed622ff2c3b?w=800\"\n",[137,70980,70981],{"class":139,"line":662},[137,70982,70983],{},"      caption: \"Dramatic peaks at golden hour\"\n",[137,70985,70986],{"class":139,"line":667},[137,70987,70988],{},"    - url: \"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1519681393784-d120267933ba?w=800\"\n",[137,70990,70991],{"class":139,"line":786},[137,70992,70993],{},"      caption: \"Starry night over snow-capped mountains\"\n",[137,70995,70996],{"class":139,"line":798},[137,70997,70998],{},"    - url: \"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1486870591958-9b9d0d1dda99?w=800\"\n",[137,71000,71001],{"class":139,"line":803},[137,71002,71003],{},"      caption: \"Misty morning in the Alps\"\n",[137,71005,71006],{"class":139,"line":931},[137,71007,70943],{},[137,71009,71010],{"class":139,"line":1568},[137,71011,516],{"emptyLinePlaceholder":515},[137,71013,71014],{"class":139,"line":1573},[137,71015,71016],{},"# Mountain Photography Guide\n",[137,71018,71019],{"class":139,"line":1578},[137,71020,516],{"emptyLinePlaceholder":515},[137,71022,71023],{"class":139,"line":1588},[137,71024,71025],{},"Capturing the majesty of mountains requires patience, preparation,\n",[137,71027,71028],{"class":139,"line":1601},[137,71029,71030],{},"and an understanding of light.\n",[137,71032,71033],{"class":139,"line":3802},[137,71034,516],{"emptyLinePlaceholder":515},[137,71036,71037],{"class":139,"line":3808},[137,71038,71039],{},"## Essential Gear\n",[137,71041,71042],{"class":139,"line":3822},[137,71043,516],{"emptyLinePlaceholder":515},[137,71045,71046],{"class":139,"line":3827},[137,71047,71048],{},"- **Wide-angle lens** (16-35mm) for sweeping landscapes\n",[137,71050,71051],{"class":139,"line":3832},[137,71052,71053],{},"- **Telephoto lens** (70-200mm) for distant peaks\n",[137,71055,71056],{"class":139,"line":3840},[137,71057,71058],{},"- **Sturdy tripod** for long exposures\n",[137,71060,71061],{"class":139,"line":3846},[137,71062,71063],{},"- **ND filters** for controlling light\n",[137,71065,71066],{"class":139,"line":3861},[137,71067,516],{"emptyLinePlaceholder":515},[137,71069,71070],{"class":139,"line":3883},[137,71071,71072],{},"## Best Times to Shoot\n",[137,71074,71075],{"class":139,"line":3896},[137,71076,516],{"emptyLinePlaceholder":515},[137,71078,71079],{"class":139,"line":3901},[137,71080,71081],{},"> \"The best light happens when most people are still sleeping.\"\n",[137,71083,71084],{"class":139,"line":3906},[137,71085,516],{"emptyLinePlaceholder":515},[137,71087,71088],{"class":139,"line":3911},[137,71089,71090],{},"Golden hour (sunrise\u002Fsunset) provides warm, dramatic lighting...\n",[137,71092,71093],{"class":139,"line":4666},[137,71094,516],{"emptyLinePlaceholder":515},[137,71096,71097],{"class":139,"line":4672},[137,71098,71099],{},"## Photo Gallery\n",[137,71101,71102],{"class":139,"line":4680},[137,71103,516],{"emptyLinePlaceholder":515},[137,71105,71106],{"class":139,"line":4711},[137,71107,71108],{},"![Golden hour ridge](https:\u002F\u002Fimages.unsplash.com\u002F...)\n",[137,71110,71111],{"class":139,"line":4716},[137,71112,71113],{},"![Night sky over peaks](https:\u002F\u002Fimages.unsplash.com\u002F...)\n",[27,71115,71116],{},"The frontmatter provides the agent with metadata:",[1003,71118,71119,71127,71133],{},[1006,71120,71121,71124,71125,9729],{},[22,71122,71123],{},"heroImage"," → triggers a ",[22,71126,67261],{},[1006,71128,71129,71132],{},[22,71130,71131],{},"tags"," → helps determine topic-based styling (nature → greens)",[1006,71134,71135,71138,71139,71141],{},[22,71136,71137],{},"images"," array → signals that an ",[22,71140,67229],{}," might be appropriate",[27,71143,71144],{},"The content structure also influences component choices:",[1003,71146,71147,71152,71157,71162],{},[1006,71148,71149,71150,9729],{},"Lists → ",[22,71151,67279],{},[1006,71153,71154,71155,9729],{},"Blockquotes → ",[22,71156,67241],{},[1006,71158,71159,71160,9729],{},"Multiple images → ",[22,71161,67229],{},[1006,71163,71164,71165,9729],{},"Code blocks → ",[22,71166,67235],{},[27,71168,71169],{},"The agent analyses both metadata and content to determine the best layout and styling.",[104,71171,71173],{"id":71172},"wire-everything-together",[42,71174,71175],{},"Wire Everything Together",[27,71177,71178,71179,71181],{},"Update ",[22,71180,5140],{}," scripts to run both the agent server and Vite client concurrently:",[128,71183,71185],{"className":36884,"code":71184,"language":29196,"meta":133,"style":133},"{\n  \"scripts\": {\n    \"dev\": \"concurrently \\\"npm run server\\\" \\\"npm run client\\\"\",\n    \"server\": \"tsx watch server\u002Findex.ts\",\n    \"client\": \"vite\"\n  }\n}\n",[22,71186,71187,71191,71198,71228,71240,71250,71254],{"__ignoreMap":133},[137,71188,71189],{"class":139,"line":140},[137,71190,15971],{"class":157},[137,71192,71193,71196],{"class":139,"line":173},[137,71194,71195],{"class":284},"  \"scripts\"",[137,71197,1819],{"class":157},[137,71199,71200,71203,71205,71208,71211,71214,71216,71219,71222,71224,71226],{"class":139,"line":188},[137,71201,71202],{"class":284},"    \"dev\"",[137,71204,726],{"class":157},[137,71206,71207],{"class":284},"\"concurrently ",[137,71209,71210],{"class":364},"\\\"",[137,71212,71213],{"class":284},"npm run server",[137,71215,71210],{"class":364},[137,71217,71218],{"class":364}," \\\"",[137,71220,71221],{"class":284},"npm run client",[137,71223,71210],{"class":364},[137,71225,28792],{"class":284},[137,71227,1961],{"class":157},[137,71229,71230,71233,71235,71238],{"class":139,"line":269},[137,71231,71232],{"class":284},"    \"server\"",[137,71234,726],{"class":157},[137,71236,71237],{"class":284},"\"tsx watch server\u002Findex.ts\"",[137,71239,1961],{"class":157},[137,71241,71242,71245,71247],{"class":139,"line":278},[137,71243,71244],{"class":284},"    \"client\"",[137,71246,726],{"class":157},[137,71248,71249],{"class":284},"\"vite\"\n",[137,71251,71252],{"class":139,"line":291},[137,71253,3462],{"class":157},[137,71255,71256],{"class":139,"line":297},[137,71257,510],{"class":157},[27,71259,22292,71260,71262],{},[22,71261,13489],{}," file in the project root with your Gemini API key:",[128,71264,71267],{"className":71265,"code":71266,"language":5189},[5187],"GEMINI_API_KEY=your_api_key_here\n",[22,71268,71266],{"__ignoreMap":133},[27,71270,71271,71272,1017],{},"You can get a free API key from ",[45,71273,66849],{"href":71274,"target":2716,"rel":71275},"https:\u002F\u002Faistudio.google.com\u002Fapi-keys",[2718,2719],[123,71277,71279],{"id":71278},"running-the-demo",[42,71280,71281],{},"Running the Demo",[128,71283,71284],{"className":8665,"code":21926,"language":8667,"meta":133,"style":133},[22,71285,71286],{"__ignoreMap":133},[137,71287,71288,71290,71292],{"class":139,"line":140},[137,71289,9536],{"class":147},[137,71291,9578],{"class":284},[137,71293,9581],{"class":284},[27,71295,71296],{},"This starts:",[1003,71298,71299,71307],{},[1006,71300,71301,71304,71305],{},[42,71302,71303],{},"Agent server"," at ",[22,71306,25546],{},[1006,71308,71309,71304,71312],{},[42,71310,71311],{},"Vite client",[22,71313,66904],{},[27,71315,61028,71316,71318],{},[22,71317,66904],{}," and click different articles. Watch how:",[1003,71320,71321,71327,71333,71339,71344],{},[1006,71322,71323,71326],{},[42,71324,71325],{},"Mountain Photography"," renders with earthy greens and an image gallery",[1006,71328,71329,71332],{},[42,71330,71331],{},"Space Exploration"," renders with cosmic purples and deep blues",[1006,71334,71335,71338],{},[42,71336,71337],{},"Italian Cooking"," renders with warm oranges and appetising styling",[1006,71340,71341,71343],{},[42,71342,5296],{}," renders with technical blues and code-focused layout",[1006,71345,71346,71349],{},[42,71347,71348],{},"Building AI Chatbot"," renders with modern teals and code blocks",[27,71351,71352,71353,71356],{},"The same renderer, the same components - but completely different UIs based on content. The agent analyses each article's topic and structure, then chooses both the layout ",[30,71354,71355],{},"and"," the colour palette to match.",[104,71358,71359],{"id":2566},[42,71360,2567],{},[27,71362,71363],{},"A2UI represents a paradigm shift in how we think about AI-powered interfaces. AI agents can now describe rich, styled UIs that your application renders safely.",[27,71365,71366,71367,71370],{},"The key insight from this demo: ",[42,71368,71369],{},"content can drive design decisions",". By giving the LLM both component options and styling parameters, we create interfaces that feel intentionally designed - even though they're generated on the fly.",[27,71372,71373,71374,71376,71377,71379],{},"The future of UI isn't just about ",[30,71375,66602],{}," we display, but ",[30,71378,66606],{}," we display it based on context. A2UI gives you the tools to build it today.",[27,71381,71382,71383,1017],{},"You can find the complete project code in the ",[45,71384,71386],{"href":66916,"target":2716,"rel":71385},[2718,2719],"GitHub repository",[2617,71388,71389],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":133,"searchDepth":173,"depth":173,"links":71391},[71392,71395,71399,71402,71403,71404,71410,71411,71417,71418,71419,71422],{"id":66629,"depth":173,"text":66632,"children":71393},[71394],{"id":66649,"depth":188,"text":66650},{"id":66696,"depth":173,"text":66697,"children":71396},[71397,71398],{"id":66729,"depth":188,"text":66730},{"id":66772,"depth":188,"text":66773},{"id":59180,"depth":173,"text":59181,"children":71400},[71401],{"id":66828,"depth":188,"text":66831},{"id":47891,"depth":173,"text":47892},{"id":67172,"depth":173,"text":67175},{"id":67699,"depth":173,"text":67702,"children":71405},[71406,71407,71408,71409],{"id":67712,"depth":188,"text":67713},{"id":67971,"depth":188,"text":67972},{"id":68318,"depth":188,"text":68319},{"id":68532,"depth":188,"text":68533},{"id":68818,"depth":173,"text":68821},{"id":69350,"depth":173,"text":69353,"children":71412},[71413,71414,71415,71416],{"id":69363,"depth":188,"text":69364},{"id":69698,"depth":188,"text":69699},{"id":70039,"depth":188,"text":70040},{"id":70390,"depth":188,"text":70391},{"id":70458,"depth":173,"text":70461},{"id":70916,"depth":173,"text":70919},{"id":71172,"depth":173,"text":71175,"children":71420},[71421],{"id":71278,"depth":188,"text":71281},{"id":2566,"depth":173,"text":2567},"Learn how to build adaptive interfaces where the AI decides not just what to show, but how to style it. Discover Google's A2UI protocol, an open-source standard that enables AI agents to dynamically generate and style UI components based on content analysis. This guide walks you through building a content-driven blog application using Gemini 2.5 Flash and Lit Web Components.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1766977382\u002Fblog\u002Fbuilding-content-adaptive-interfaces-with-googles-a2ui\u002Fhero-building-content-adaptive-interfaces-with-googles-a2ui_jc2fg2",[71426,66642,71427,71428,66625,71429,71430,71431,71432,5296,12817,22707,71433,66849,71434,71435,71436,71437,71438],"Google A2UI","AI-powered interfaces","Content-adaptive UI","Lit Web Components","AI UI generation","Dynamic UI rendering","AI agents","Express.js","AI content styling","Declarative UI","Progressive rendering","Conversational assistants","Personalised experiences",{},"\u002F2025\u002F12\u002F29\u002Fbuilding-content-adaptive-interfaces-with-googles-a2ui","29th December 2025",{"title":66561,"description":71423},"2025\u002F12\u002F29\u002Fbuilding-content-adaptive-interfaces-with-googles-a2ui","QTUGXbDGVuifcTI-JiXbaoxqzbV5lJV4z2CWx--lpaQ",{"id":71446,"title":71447,"articleTags":71448,"author":11,"blog":12,"body":71451,"description":71901,"extension":2649,"image":71902,"keywords":71903,"meta":71919,"navigation":515,"path":71920,"published":71921,"readTime":798,"seo":71922,"stem":71923,"type":2662,"__hash__":71924},"content\u002F2026\u002F01\u002F04\u002Fblogging-in-2026-building-your-own-knowledge-base.md","Blogging in 2026: Building Your Own Knowledge Base",[71449,52208,71450],"Productivity","Advice",{"type":14,"value":71452,"toc":71882},[71453,71456,71470,71472,71476,71481,71488,71492,71508,71511,71514,71518,71521,71524,71527,71530,71534,71537,71540,71543,71546,71555,71558,71561,71565,71568,71573,71576,71580,71599,71602,71605,71608,71611,71616,71619,71622,71629,71655,71658,71662,71669,71672,71675,71678,71681,71684,71687,71694,71697,71700,71703,71707,71712,71715,71722,71726,71729,71732,71736,71739,71742,71746,71749,71752,71755,71758,71765,71770,71774,71777,71784,71787,71796,71800,71803,71806,71810,71813,71816,71819,71822,71825,71828,71832,71835,71838,71841,71845,71848,71851,71854,71857,71860,71863,71866,71869,71873,71876,71879],[17,71454,71447],{"id":71455},"blogging-in-2026-building-your-own-knowledge-base",[27,71457,71458],{},[30,71459,71460,36,71462,40,71464],{},[33,71461],{"value":35},[33,71463],{"value":39},[42,71465,71466],{},[45,71467,71468],{"href":47},[33,71469],{"value":50},[52,71471],{":tags":54},[56,71473],{":audio-src":71474,":transcript-src":71475},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F01\u002F04\u002Fblogging-in-2026-building-your-own-knowledge-base\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F01\u002F04\u002Fblogging-in-2026-building-your-own-knowledge-base\u002Fsummary.json",[27,71477,71478],{},[63,71479],{"alt":12847,"src":71480},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1767486674\u002Fblog\u002Fblogging-in-2026-building-your-own-knowledge-base\u002Fare-blogs-dying-because-of-ai-not-quite-2_djhvpq",[27,71482,71483,71484,71487],{},"I published my first ",[47718,71485,71486],{"to":2658},"tech blog article"," in 2021. Since then, I've written 39 more - making this my 40th. That number isn't massive, but it's meaningful to me. Hitting this milestone in 2026 felt like the right moment to pause and reflect on what blogging has meant to me - and how dramatically it has changed along the way.",[104,71489,71491],{"id":71490},"my-blogging-journey","My Blogging Journey",[27,71493,71494,71495,71502,71503,1017],{},"For a long time, I thought about starting a blog about my development career. I wanted to share things I was learning, small discoveries, and practical tips - but I wasn't convinced I had anything valuable to say. That mindset changed after I read ",[45,71496,71499],{"href":71497,"target":2716,"rel":71498},"https:\u002F\u002Fswyx.gumroad.com\u002Fl\u002FbAZJq",[2718,2719],[30,71500,71501],{},"The Coding Career Handbook"," by ",[45,71504,71507],{"href":71505,"target":2716,"rel":71506},"https:\u002F\u002Fx.com\u002Fswyx",[2718,2719],"Shawn Wang",[27,71509,71510],{},"One piece of advice from that book stuck with me: start your own blog and share things you've learned that don't already have clear answers online. It sounds simple, but for me, it was a turning point. After reading that book in 2020, I finally took the leap and published my first article the following year.",[27,71512,71513],{},"Before that, I genuinely believed I wasn't \"good enough\" to contribute to the tech space. I worried that smarter people would call me out, point out mistakes, or dismiss my ideas entirely. Looking back, that fear kept me silent far longer than it should have.",[104,71515,71517],{"id":71516},"how-i-used-to-write-articles-before-ai","How I Used to Write Articles (Before AI)",[27,71519,71520],{},"Almost every article began with code. I'd experiment with a new framework feature, a language update, or a small side project. Sometimes it was driven by frustration: a problem I couldn't find a solution for anywhere online.",[27,71522,71523],{},"Those experiments could take days, weeks, or even months. Quite a few never saw the light of day. But when something finally felt solid - something I would actually use myself - that's when I'd consider turning it into a blog post.",[27,71525,71526],{},"Writing the article itself was slow and deliberate. I'd spend weeks shaping a draft. Since English isn't my first language, grammar and spelling were always a challenge. I'd then ask my partner - whose first language is English - to proofread it, which added another layer of waiting and coordination.",[27,71528,71529],{},"And that still wasn't the end. I needed a cover image, often involving a fair bit of Photoshop work. Publishing each article required a serious time commitment.",[104,71531,71533],{"id":71532},"fast-forward-five-years","Fast-Forward Five Years",[27,71535,71536],{},"Compare that process to how I publish articles today, and the difference is night and day - even though my career as a blogger is relatively short, only half a decade.",[27,71538,71539],{},"I now spend far less time scaffolding new projects. Coding agents truly took over in 2025, helping not just me, but the entire industry, move faster. Exploring new ideas no longer feels heavy or slow - and because the build phase is quicker, I can jump into writing much earlier.",[27,71541,71542],{},"Another big shift is context. Since agents help me build the codebase, they already \"know\" what the project does and why certain decisions were made. That makes generating an initial draft surprisingly easy. The first version isn't perfect - but it doesn't need to be.",[27,71544,71545],{},"That draft is just the starting point. I still do quite a bit of manual work - removing fluff, reshaping explanations, adding missing insights, and making sure the article sounds good to readers and brings real value. I think carefully about visuals, structure, and how someone else will experience the content.",[27,71547,71548,71549,71554],{},"Once that's done, I lean on tools like ",[45,71550,71553],{"href":71551,"target":2716,"rel":71552},"https:\u002F\u002Fwww.notion.com\u002Fproduct\u002Fai",[2718,2719],"Notion AI"," to clean up grammar and polish the language. What used to take weeks now takes hours.",[27,71556,71557],{},"Finally, I use tools like Google Gemini or ChatGPT to generate cover images, diagrams, and flowcharts. Visuals that once took days of effort are now quick to iterate on.",[27,71559,71560],{},"The result? I'm easily five times more productive than I was before.",[104,71562,71564],{"id":71563},"so-are-blogs-dying","So… Are Blogs Dying?",[27,71566,71567],{},"Over the past year, my feeds have been flooded with bold claims: blogs are dead, AI has killed content, no one reads anymore, LinkedIn posts are garbage. It's a familiar narrative - and an understandable fear.",[27,71569,71570],{},[42,71571,71572],{},"But is this really the end of blogging? I don't think so. Not even close.",[27,71574,71575],{},"In fact, I find more valuable content online today than I used to. People are publishing more frequently, sharing their personal struggles, and celebrating their \"wins.\" Because the barrier to entry is lower, everyone feels more productive. We are learning from each other at a faster rate than ever before. To me, this isn't a funeral for blogging - it's a win-win situation for the entire community.",[104,71577,71579],{"id":71578},"efficiency-didnt-kill-creation-it-multiplied-it","Efficiency Didn't Kill Creation - It Multiplied It",[27,71581,71582,71583,71502,71590,71595,71596,1017],{},"I recently came across an article called ",[45,71584,71587],{"href":71585,"target":2716,"rel":71586},"https:\u002F\u002Faddyosmani.com\u002Fblog\u002Fthe-efficiency-paradox\u002F",[2718,2719],[30,71588,71589],{},"The Efficiency Paradox",[45,71591,71594],{"href":71592,"target":2716,"rel":71593},"https:\u002F\u002Fx.com\u002Faddyosmani",[2718,2719],"Addy Osmani"," that captures something we've seen repeatedly in software: every time we make creation easier, we don't create less - we create ",[30,71597,71598],{},"more",[27,71600,71601],{},"Higher-level languages didn't reduce the number of programs. Frameworks didn't shrink the number of apps. Cloud infrastructure didn't make teams obsolete. Each step removed friction and expanded what was possible.",[27,71603,71604],{},"The same pattern applies to writing.",[27,71606,71607],{},"In my case, AI hasn't replaced blogging - it's removed the friction around it. I can ship code faster, explore ideas more freely, and move from \"this might be interesting\" to \"this is worth sharing\" without weeks of overhead. That efficiency doesn't disappear. It compounds.",[27,71609,71610],{},"Here's what that looks like in practice. Since I started blogging, I've published 40 articles, including this one. Broken down by year:",[27,71612,71613],{},[63,71614],{"alt":71579,"src":71615},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1767486674\u002Fblog\u002Fblogging-in-2026-building-your-own-knowledge-base\u002Fefficiency-didnt-kill-creation-it-multiplied-it_bythqf",[27,71617,71618],{},"That jump in 2025 wasn't accidental. It came from better tooling around LLMs - agentic workflows that let me move faster without compromising quality.",[27,71620,71621],{},"And here's what often gets missed:",[27,71623,71624,71625,71628],{},"AI can generate words faster. That doesn't make writing meaningless. It makes ",[42,71626,71627],{},"decisions"," more meaningful.",[1003,71630,71631,71637,71643,71649],{},[1006,71632,71633,71636],{},[42,71634,71635],{},"Getting unstuck:"," When you have a topic but can't get the draft moving, AI helps brainstorm approaches that make tricky sections easier for readers to follow.",[1006,71638,71639,71642],{},[42,71640,71641],{},"Simplifying content:"," If you tend to overwrite, AI can help trim and simplify sentences so key points land clearly.",[1006,71644,71645,71648],{},[42,71646,71647],{},"Spotting weak ideas:"," When a concept isn't strong enough to publish, AI helps you test, refine, or rethink it before hitting \"publish.\"",[1006,71650,71651,71654],{},[42,71652,71653],{},"Grammar and clarity:"," If English isn't your first language, AI catches mistakes and smooths out phrasing - saving time and building confidence.",[27,71656,71657],{},"Efficiency didn't remove the human from the loop - it moved the human to the most important part of it.",[104,71659,71661],{"id":71660},"why-i-write","Why I Write",[27,71663,71664,71665,71668],{},"This brings me to the question I haven't really answered yet: ",[30,71666,71667],{},"what actually keeps me writing?"," What do I get out of all this?",[27,71670,71671],{},"If I'm being honest, the most important reader of my blog has always been… me. I know how that sounds 🙂 but it's true. I've gone back to my own articles more times than I can count - usually when I've forgotten how I solved a problem months or years earlier.",[27,71673,71674],{},"Over time, my blog has quietly become a second brain. It's a searchable record of my thinking, experiments, trade-offs, and mistakes. Not polished textbook knowledge - but lived knowledge. And for me, that's far more valuable than perfectly curated tutorials written for an imaginary audience.",[27,71676,71677],{},"Another thing that doesn't get talked about enough: you don't truly understand something until you try to explain it.",[27,71679,71680],{},"Most of my posts don't start with expertise. They start with curiosity. I stumble across a new tool, pattern, or idea I want to explore. I try it. I break things. I hit dead ends. Then I write about what actually happened.",[27,71682,71683],{},"Writing forces me to slow down and be honest about what I do and don't understand. If I can't explain something clearly to a reader, that's usually a sign I don't understand it well enough myself. That friction isn't a problem - it's the point. The real learning happens in those \"wait, that's not quite right\" moments.",[27,71685,71686],{},"There's also a benefit I didn't fully appreciate until recently, especially relevant in an AI-driven world: well-documented blogs are AI-friendly by default.",[27,71688,71689,71690,71693],{},"Most of my articles come with a GitHub repository and a clear explanation of ",[30,71691,71692],{},"why"," things are built the way they are. I've started referencing my own posts when working with coding agents - asking them to read an article I wrote before extending a feature or refactoring a project. That feedback loop is surprisingly powerful. The blog isn't just content anymore - it's part of my tooling.",[27,71695,71696],{},"I'd write this blog even if I had zero readers.",[27,71698,71699],{},"Because the person who benefits most is my future self.",[27,71701,71702],{},"These posts save me time, sharpen my thinking, and make me a better engineer. If others get value from them too, that's a bonus - but it's no longer the primary reason I write.",[104,71704,71706],{"id":71705},"my-approach","My Approach",[27,71708,71709],{},[63,71710],{"alt":71706,"src":71711},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1767486674\u002Fblog\u002Fblogging-in-2026-building-your-own-knowledge-base\u002Fmy-approach-2_cmzrzy",[27,71713,71714],{},"This post wouldn't feel complete without sharing how I actually write today - my workflow and the tools I use to turn an idea into a published article.",[27,71716,71717,71718,71721],{},"This is ",[30,71719,71720],{},"my current approach",". It wasn't always like this. For years, the process was slower, more manual, and far more time-consuming. What I'm describing is a workflow I gradually adopted over the past year as my tools - and my mindset - changed.",[123,71723,71725],{"id":71724},"starting-with-an-idea","Starting with an Idea",[27,71727,71728],{},"Like most things I write, everything starts with an idea.",[27,71730,71731],{},"That idea might come from a new CSS feature, a JavaScript or TypeScript language update, an interesting AI experiment, or a pattern I see someone else exploring. Sometimes it's genuinely new, other times it's an existing idea I want to investigate deeper and add my own perspective to.",[123,71733,71735],{"id":71734},"research-phase","Research Phase",[27,71737,71738],{},"Once I have a rough idea, I move into research.",[27,71740,71741],{},"Today, that looks very different from traditional \"Googling.\" I rely heavily on tools like ChatGPT and Gemini - especially their web search and deep research capabilities. For me, these tools have largely replaced classic search engines. I use them to gather context, surface sources, and challenge my assumptions. That said, I don't blindly trust the output. I still manually review sources, decide what's relevant, and filter noise from genuinely useful information.",[123,71743,71745],{"id":71744},"proving-the-idea","Proving the Idea",[27,71747,71748],{},"If the article involves code - which most of my posts do - the next step is proving the idea.",[27,71750,71751],{},"This phase decides whether an idea becomes a blog post or gets abandoned. If I can't make it work in practice, it's usually not worth writing about. The tools I use change from time to time, but the approach stays the same. I work with coding agents - Claude Code, Antigravity, GitHub Copilot, and similar tools - to build out the codebase and experiment with the idea.",[27,71753,71754],{},"The key is that I guide the agent closely. I provide context, constraints, and often the research sources I gathered earlier. This isn't a one-shot process. It requires iteration, feedback, and supervision. These tools are far better than they were a year or two ago, but they still need careful direction.",[27,71756,71757],{},"I also use a coding agent even for small changes. The reason is simple: context. The more context the agent has about the project, the more useful it becomes later - especially when drafting the article.",[27,71759,71760,71761,71764],{},"Once the idea is stable and proven, and I'm confident it's worth documenting, I generate a ",[42,71762,71763],{},"README.md"," for the repository and publish the code on GitHub. That becomes the foundation for the written piece.",[3244,71766,71767],{},[27,71768,71769],{},"Not every article follows this step. This post, for example, has no code - so there's no coding agent phase here.",[123,71771,71773],{"id":71772},"drafting-the-article","Drafting the Article",[27,71775,71776],{},"Next comes the draft.",[27,71778,71779,71780,71783],{},"If there ",[30,71781,71782],{},"is"," a codebase, I start by asking the coding agent to produce an initial draft. At this point, the agent has the most context - it understands the code, the decisions, and the trade-offs - so it's surprisingly effective at generating a beginner-friendly, step-by-step outline.",[27,71785,71786],{},"That first draft is never great. It lacks personality and voice. But that's fine. It's a starting point.",[27,71788,71789,71790,71795],{},"I then move the draft into ",[45,71791,71794],{"href":71792,"target":2716,"rel":71793},"https:\u002F\u002Fwww.notion.com\u002F",[2718,2719],"Notion",", my primary writing tool. From there, I reshape everything: reordering sections, rewriting explanations, adding missing context, cutting unnecessary details, and injecting my own experiences and opinions.",[123,71797,71799],{"id":71798},"polishing-with-ai-and-voice","Polishing with AI and Voice",[27,71801,71802],{},"Recently, I've also started using voice dictation more. I'll speak ideas out loud while walking, driving, or doing household chores. It's a small change, but it's made a big difference in keeping momentum - letting me add content even when I'm not at my desk.",[27,71804,71805],{},"Once the content is there - and it doesn't need to be perfect - I use Notion AI for polishing. I don't ask it to generate new content. I use it strictly to improve clarity, grammar, and flow. The article remains mine, the AI just helps clean it up.",[123,71807,71809],{"id":71808},"creating-visuals","Creating Visuals",[27,71811,71812],{},"The final step is visuals.",[27,71814,71815],{},"For cover images, diagrams, flowcharts, or charts, I rely on image generation models like Gemini and ChatGPT. For cover images, I'll often feed the entire article to the model and ask it to generate multiple prompt ideas from different angles. I refine the prompt I like most and generate variations until something feels right.",[27,71817,71818],{},"I'm aware that AI-generated images are often recognisable - and I'm okay with that. Perfection isn't the goal. Clarity and usefulness are.",[27,71820,71821],{},"For charts or data-driven visuals, I provide the data directly and have the model generate charts (often via Python), then iterate on them - making them more playful or better aligned with my branding.",[27,71823,71824],{},"As you can see, AI shows up at almost every step of this process.",[27,71826,71827],{},"That's just the reality of how I work today, and I'm not particularly precious about it.",[123,71829,71831],{"id":71830},"publishing","Publishing",[27,71833,71834],{},"Once everything is ready, I publish the article on my personal blog. Since my blog articles are written in Markdown and Notion content converts cleanly, the final step is simple: copy, paste, publish.",[27,71836,71837],{},"And that's it.",[27,71839,71840],{},"That's how an idea turns into a blog post for me, right now.",[104,71842,71844],{"id":71843},"how-i-use-my-own-content","How I Use My Own Content",[27,71846,71847],{},"Earlier, I mentioned that the most important reader of my blog has always been me. That isn't a throwaway line - it's the core reason this blog exists.",[27,71849,71850],{},"The articles I write are long-lived documents. I return to them months or even years later when I need to remember how I approached a problem, why I made a certain trade-off, or what didn't work the first time.",[27,71852,71853],{},"In the early days, that reuse was completely manual. I'd open an old article, reread it top to bottom, and retrace my steps line by line until the solution clicked.",[27,71855,71856],{},"That has changed.",[27,71858,71859],{},"Today, I still revisit my own writing - but now I reference my articles and repositories directly inside coding agents. When I'm working on a new feature or revisiting an old idea, I'll point the agent to a specific blog post or GitHub repo and ask it to use that context before suggesting changes or extensions.",[27,71861,71862],{},"That shift has been surprisingly powerful.",[27,71864,71865],{},"My blog has effectively become a structured knowledge base - one I can feed back into my tools.",[27,71867,71868],{},"One of my goals this year is to build an MCP server over my entire blog - making the archive natively accessible to agents. When that's ready, I'll update this section and document the process. I'm confident it'll become another feedback loop: writing improves tooling, and better tooling makes writing even more valuable.",[104,71870,71872],{"id":71871},"final-thoughts","Final Thoughts",[27,71874,71875],{},"After 40 articles, I've learned that blogging is about keeping up with yourself - not the internet.",[27,71877,71878],{},"AI-generated content is accelerating - more tools, more automation, more output. That's not a problem. It's how every major shift in technology begins. We're in the middle of another adjustment period, and like all revolutions, it feels noisy before it settles.",[27,71880,71881],{},"This is why I encourage more people to build their own knowledge base. Writing clarifies thinking and creates a trail you can return to later. These articles now save me time, guide my decisions, and feed directly back into the tools I use every day.",{"title":133,"searchDepth":173,"depth":173,"links":71883},[71884,71885,71886,71887,71888,71889,71890,71899,71900],{"id":71490,"depth":173,"text":71491},{"id":71516,"depth":173,"text":71517},{"id":71532,"depth":173,"text":71533},{"id":71563,"depth":173,"text":71564},{"id":71578,"depth":173,"text":71579},{"id":71660,"depth":173,"text":71661},{"id":71705,"depth":173,"text":71706,"children":71891},[71892,71893,71894,71895,71896,71897,71898],{"id":71724,"depth":188,"text":71725},{"id":71734,"depth":188,"text":71735},{"id":71744,"depth":188,"text":71745},{"id":71772,"depth":188,"text":71773},{"id":71798,"depth":188,"text":71799},{"id":71808,"depth":188,"text":71809},{"id":71830,"depth":188,"text":71831},{"id":71843,"depth":173,"text":71844},{"id":71871,"depth":173,"text":71872},"After 40 tech articles over five years, I reflect on how AI has transformed my blogging workflow. From slow, manual drafts to efficient AI-assisted writing, I share my approach to building a personal knowledge base, why I write primarily for my future self, and how blogs have become more valuable than ever in the AI era.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1767486674\u002Fblog\u002Fblogging-in-2026-building-your-own-knowledge-base\u002Fare-blogs-dying-because-of-ai-not-quite-2_djhvpq",[71904,71905,71906,71907,71908,71909,71910,71911,71912,71913,71914,71915,71916,71917,71918],"blogging in 2026","AI-assisted writing","knowledge base","personal blog","tech blogging","AI workflow","coding agents","writing with AI","blog productivity","second brain","content creation","AI tools for writers","blogging journey","technical writing","future of blogging",{},"\u002F2026\u002F01\u002F04\u002Fblogging-in-2026-building-your-own-knowledge-base","4th January 2026",{"title":71447,"description":71901},"2026\u002F01\u002F04\u002Fblogging-in-2026-building-your-own-knowledge-base","r5cE6kKSmf0RZL2PvWPLTEC7PFeAidpoRJDLF_OlvGk",{"id":71926,"title":71927,"articleTags":71928,"author":11,"blog":12,"body":71929,"description":75877,"extension":2649,"image":75878,"keywords":75879,"meta":75895,"navigation":515,"path":75896,"published":75897,"readTime":803,"seo":75898,"stem":75899,"type":2662,"__hash__":75900},"content\u002F2026\u002F01\u002F18\u002Funderstanding-modern-rpc-frameworks-how-they-work-and-when-to-use-them.md","Understanding Modern RPC Frameworks: How They Work and When to Use Them",[12817,2669,52208],{"type":14,"value":71930,"toc":75818},[71931,71934,71948,71950,71954,71959,71981,71990,71994,71997,72008,72012,72015,72020,72024,72246,72249,72349,72355,72358,72384,72387,72392,72396,72428,72431,72435,72442,72445,72451,72490,72493,72498,72502,72505,72515,72518,72524,72527,72612,72615,72618,72621,72625,72628,72631,72634,72637,72640,72643,72647,72650,72669,72681,72684,72706,72709,72712,72738,72745,72749,72752,72757,72760,72763,72767,72770,72773,72776,72790,72793,72797,73073,73076,73080,73222,73225,73229,73240,73244,73258,73261,73264,73268,73271,73274,73278,73295,73298,73302,73363,73369,73373,73416,73419,73423,73727,73730,73734,73861,73864,73867,73881,73884,73901,73904,73907,73911,73914,73921,73924,73938,73941,73945,73976,73980,73991,73994,73998,74069,74084,74088,74326,74330,74493,74497,74531,74535,74567,74570,74574,74585,74589,74592,74595,74599,74827,74831,74981,74985,74995,75009,75012,75047,75050,75070,75073,75077,75080,75084,75087,75091,75435,75439,75600,75603,75634,75637,75651,75657,75660,75666,75669,75721,75724,75730,75733,75775,75778,75782,75785,75788,75791,75797,75805,75808,75815],[17,71932,71927],{"id":71933},"understanding-modern-rpc-frameworks-how-they-work-and-when-to-use-them",[27,71935,71936],{},[30,71937,71938,36,71940,40,71942],{},[33,71939],{"value":35},[33,71941],{"value":39},[42,71943,71944],{},[45,71945,71946],{"href":47},[33,71947],{"value":50},[52,71949],{":tags":54},[56,71951],{":audio-src":71952,":transcript-src":71953},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F01\u002F18\u002Funderstanding-modern-rpc-frameworks-how-they-work-and-when-to-use-them\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F01\u002F18\u002Funderstanding-modern-rpc-frameworks-how-they-work-and-when-to-use-them\u002Fsummary.json",[27,71955,71956],{},[63,71957],{"alt":12847,"src":71958},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1768564822\u002Fblog\u002Funderstanding-modern-rpc-frameworks-how-they-work-and-when-to-use-them\u002Funderstanding-modern-rpc-frameworks-how-they-work-and-when-to-use-them_pkdooe",[27,71960,71961,71962,71967,71968,71970,71971,164,71973,164,71975,14528,71978,1017],{},"When building a web application where the frontend needs to communicate with the backend, we almost always reach for ",[45,71963,71966],{"href":71964,"target":2716,"rel":71965},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FREST",[2718,2719],"REST",". It's what we know best, well-documented and works beautifully with the browser's ",[22,71969,59737],{}," API using HTTP methods like ",[22,71972,22312],{},[22,71974,22316],{},[22,71976,71977],{},"PUT",[22,71979,71980],{},"DELETE",[27,71982,71983,71984,71989],{},"Client-server communication typically means creating ",[45,71985,71988],{"href":71986,"target":2716,"rel":71987},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCreate,_read,_update_and_delete",[2718,2719],"CRUD"," API endpoints, passing parameters in requests, and handling responses from the server.",[104,71991,71993],{"id":71992},"a-brief-look-at-rest","A Brief Look at REST",[27,71995,71996],{},"You're probably already familiar with REST - most of us are. Since we're here to talk about RPC, we won't spend much time on this. But it's worth briefly revisiting REST to understand why RPC might be a compelling alternative.",[27,71998,71999,72000,72007],{},"REST has dominated web API design since the early 2000s, when ",[45,72001,72004],{"href":72002,"target":2716,"rel":72003},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FRoy_Fielding",[2718,2719],[30,72005,72006],{},"Roy Fielding"," introduced it in his doctoral dissertation. It quickly became the standard for client-server communication, and for good reason: it's simple, leverages standard HTTP methods, and works seamlessly with browsers.",[104,72009,72011],{"id":72010},"how-rest-works","How REST Works",[27,72013,72014],{},"Let's walk through a simple example: adding two numbers.",[3244,72016,72017],{},[27,72018,72019],{},"All examples in this article use TypeScript in Node.js - my primary language and what I work with daily. Feel free to adapt these concepts to whatever language you're most comfortable with. The principles remain the same.",[123,72021,72023],{"id":72022},"rest-server","REST Server",[128,72025,72027],{"className":13299,"code":72026,"language":13301,"meta":133,"style":133},"import http from \"http\";\n\nhttp.createServer((req, res) => {\n    if (req.method === \"POST\" && req.url === \"\u002Fcalculate\") {\n        let body = \"\";\n\n        req.on(\"data\", (chunk) => (body += chunk));\n        req.on(\"end\", () => {\n            const { a, b } = JSON.parse(body);\n            res.end(JSON.stringify({ result: a + b }));\n        });\n    }\n}).listen(3000, () => {\n    console.log(\"REST server running on http:\u002F\u002Flocalhost:3000\");\n});\n",[22,72028,72029,72043,72047,72069,72093,72106,72110,72140,72157,72181,72204,72208,72212,72229,72242],{"__ignoreMap":133},[137,72030,72031,72033,72036,72038,72041],{"class":139,"line":140},[137,72032,10287],{"class":143},[137,72034,72035],{"class":157}," http ",[137,72037,10954],{"class":143},[137,72039,72040],{"class":284}," \"http\"",[137,72042,3276],{"class":157},[137,72044,72045],{"class":139,"line":173},[137,72046,516],{"emptyLinePlaceholder":515},[137,72048,72049,72052,72055,72057,72059,72061,72063,72065,72067],{"class":139,"line":188},[137,72050,72051],{"class":157},"http.",[137,72053,72054],{"class":147},"createServer",[137,72056,2774],{"class":157},[137,72058,9133],{"class":161},[137,72060,164],{"class":157},[137,72062,9138],{"class":161},[137,72064,219],{"class":157},[137,72066,222],{"class":143},[137,72068,256],{"class":157},[137,72070,72071,72073,72076,72078,72081,72083,72086,72088,72091],{"class":139,"line":269},[137,72072,24696],{"class":143},[137,72074,72075],{"class":157}," (req.method ",[137,72077,5502],{"class":143},[137,72079,72080],{"class":284}," \"POST\"",[137,72082,35905],{"class":143},[137,72084,72085],{"class":157}," req.url ",[137,72087,5502],{"class":143},[137,72089,72090],{"class":284}," \"\u002Fcalculate\"",[137,72092,170],{"class":157},[137,72094,72095,72097,72100,72102,72104],{"class":139,"line":278},[137,72096,65561],{"class":143},[137,72098,72099],{"class":157}," body ",[137,72101,253],{"class":143},[137,72103,4607],{"class":284},[137,72105,3276],{"class":157},[137,72107,72108],{"class":139,"line":291},[137,72109,516],{"emptyLinePlaceholder":515},[137,72111,72112,72115,72118,72120,72122,72124,72127,72129,72131,72134,72137],{"class":139,"line":297},[137,72113,72114],{"class":157},"        req.",[137,72116,72117],{"class":147},"on",[137,72119,356],{"class":157},[137,72121,38270],{"class":284},[137,72123,24531],{"class":157},[137,72125,72126],{"class":161},"chunk",[137,72128,219],{"class":157},[137,72130,222],{"class":143},[137,72132,72133],{"class":157}," (body ",[137,72135,72136],{"class":143},"+=",[137,72138,72139],{"class":157}," chunk));\n",[137,72141,72142,72144,72146,72148,72151,72153,72155],{"class":139,"line":302},[137,72143,72114],{"class":157},[137,72145,72117],{"class":147},[137,72147,356],{"class":157},[137,72149,72150],{"class":284},"\"end\"",[137,72152,4420],{"class":157},[137,72154,222],{"class":143},[137,72156,256],{"class":157},[137,72158,72159,72161,72163,72165,72167,72169,72171,72173,72175,72177,72179],{"class":139,"line":662},[137,72160,5772],{"class":143},[137,72162,8906],{"class":157},[137,72164,45],{"class":364},[137,72166,164],{"class":157},[137,72168,167],{"class":364},[137,72170,8911],{"class":157},[137,72172,253],{"class":143},[137,72174,17436],{"class":364},[137,72176,1017],{"class":157},[137,72178,17441],{"class":147},[137,72180,32215],{"class":157},[137,72182,72183,72186,72188,72190,72192,72194,72196,72199,72201],{"class":139,"line":667},[137,72184,72185],{"class":157},"            res.",[137,72187,33017],{"class":147},[137,72189,356],{"class":157},[137,72191,22554],{"class":364},[137,72193,1017],{"class":157},[137,72195,24816],{"class":147},[137,72197,72198],{"class":157},"({ result: a ",[137,72200,182],{"class":143},[137,72202,72203],{"class":157}," b }));\n",[137,72205,72206],{"class":139,"line":786},[137,72207,14079],{"class":157},[137,72209,72210],{"class":139,"line":798},[137,72211,294],{"class":157},[137,72213,72214,72217,72219,72221,72223,72225,72227],{"class":139,"line":803},[137,72215,72216],{"class":157},"}).",[137,72218,15106],{"class":147},[137,72220,356],{"class":157},[137,72222,15111],{"class":364},[137,72224,4420],{"class":157},[137,72226,222],{"class":143},[137,72228,256],{"class":157},[137,72230,72231,72233,72235,72237,72240],{"class":139,"line":931},[137,72232,493],{"class":157},[137,72234,353],{"class":147},[137,72236,356],{"class":157},[137,72238,72239],{"class":284},"\"REST server running on http:\u002F\u002Flocalhost:3000\"",[137,72241,1502],{"class":157},[137,72243,72244],{"class":139,"line":1568},[137,72245,5422],{"class":157},[123,72247,15937],{"id":72248},"rest-client",[128,72250,72252],{"className":13299,"code":72251,"language":13301,"meta":133,"style":133},"fetch(\"http:\u002F\u002Flocalhost:3000\u002Fcalculate\", {\n    method: \"POST\",\n    body: JSON.stringify({ a: 2, b: 3 }),\n})\n    .then((res) => res.json())\n    .then((data) => console.log(\"Result:\", data.result));\n",[22,72253,72254,72265,72274,72298,72302,72323],{"__ignoreMap":133},[137,72255,72256,72258,72260,72263],{"class":139,"line":140},[137,72257,59737],{"class":147},[137,72259,356],{"class":157},[137,72261,72262],{"class":284},"\"http:\u002F\u002Flocalhost:3000\u002Fcalculate\"",[137,72264,5396],{"class":157},[137,72266,72267,72270,72272],{"class":139,"line":173},[137,72268,72269],{"class":157},"    method: ",[137,72271,68971],{"class":284},[137,72273,1961],{"class":157},[137,72275,72276,72279,72281,72283,72285,72288,72290,72293,72295],{"class":139,"line":188},[137,72277,72278],{"class":157},"    body: ",[137,72280,22554],{"class":364},[137,72282,1017],{"class":157},[137,72284,24816],{"class":147},[137,72286,72287],{"class":157},"({ a: ",[137,72289,10345],{"class":364},[137,72291,72292],{"class":157},", b: ",[137,72294,60501],{"class":364},[137,72296,72297],{"class":157}," }),\n",[137,72299,72300],{"class":139,"line":269},[137,72301,13451],{"class":157},[137,72303,72304,72306,72308,72310,72312,72314,72316,72319,72321],{"class":139,"line":278},[137,72305,2748],{"class":157},[137,72307,2771],{"class":147},[137,72309,2774],{"class":157},[137,72311,9138],{"class":161},[137,72313,219],{"class":157},[137,72315,222],{"class":143},[137,72317,72318],{"class":157}," res.",[137,72320,5157],{"class":147},[137,72322,59766],{"class":157},[137,72324,72325,72327,72329,72331,72333,72335,72337,72339,72341,72343,72346],{"class":139,"line":291},[137,72326,2748],{"class":157},[137,72328,2771],{"class":147},[137,72330,2774],{"class":157},[137,72332,1942],{"class":161},[137,72334,219],{"class":157},[137,72336,222],{"class":143},[137,72338,43656],{"class":157},[137,72340,353],{"class":147},[137,72342,356],{"class":157},[137,72344,72345],{"class":284},"\"Result:\"",[137,72347,72348],{"class":157},", data.result));\n",[27,72350,72351,72352,72354],{},"The key distinction is that REST thinks in terms of ",[42,72353,28959],{},". Instead of saying \"call the add function,\" you're saying \"send a calculation request to the server.\"",[27,72356,72357],{},"Here's what's happening behind the scenes:",[2569,72359,72360,72366,72372,72375,72378],{},[1006,72361,72362,72363],{},"We send an HTTP POST request to ",[22,72364,72365],{},"\u002Fcalculate",[1006,72367,72368,72369],{},"The request body contains JSON data: ",[22,72370,72371],{},"{ a: 2, b: 3 }",[1006,72373,72374],{},"The server receives the request and parses the JSON",[1006,72376,72377],{},"The server performs the calculation",[1006,72379,72380,72381],{},"The server sends back a JSON response: ",[22,72382,72383],{},"{ result: 5 }",[27,72385,72386],{},"REST works great for many things. It's human-readable, browser-friendly, and uses standard HTTP methods.",[27,72388,72389],{},[63,72390],{"alt":15937,"src":72391},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1768564821\u002Fblog\u002Funderstanding-modern-rpc-frameworks-how-they-work-and-when-to-use-them\u002FREST-Client_ien9yp.jpg",[123,72393,72395],{"id":72394},"where-rest-can-feel-limiting","Where REST Can Feel Limiting",[1003,72397,72398,72404,72416,72422],{},[1006,72399,72400,72403],{},[42,72401,72402],{},"Complex logic requires multiple endpoints:"," As your application grows, you need to create and maintain numerous API endpoints for different operations.",[1006,72405,72406,72409,72410,72415],{},[42,72407,72408],{},"No built-in type safety:"," REST doesn't provide type safety and often requires third-party documentation tools like ",[45,72411,72414],{"href":72412,"target":2716,"rel":72413},"https:\u002F\u002Fwww.openapis.org\u002F",[2718,2719],"OpenAPI"," to bridge the gap.",[1006,72417,72418,72421],{},[42,72419,72420],{},"Resources instead of functions:"," You work with resource representations rather than calling functions directly.",[1006,72423,72424,72427],{},[42,72425,72426],{},"Verbose for simple operations:"," Even straightforward tasks require HTTP boilerplate and JSON serialisation overhead.",[27,72429,72430],{},"RESTful APIs have proven reliable and work great for CRUD operations on resources. But what if you want a different approach - one where you simply call a function?",[104,72432,72434],{"id":72433},"theres-another-way-rpc","There's Another Way: RPC",[27,72436,72437,72438,72441],{},"What if, instead of \"sending a calculation request,\" you could simply ",[42,72439,72440],{},"call a function"," on the server as if it were right there in your code?",[27,72443,72444],{},"That's exactly what RPC (Remote Procedure Call) does. It lets you call a function on another machine just like you'd call a local function.",[27,72446,72447,72448,72450],{},"For example, here's how you'd call a local ",[22,72449,34393],{}," function:",[128,72452,72454],{"className":13299,"code":72453,"language":13301,"meta":133,"style":133},"const result = await add(2, 3);\nconsole.log(result); \u002F\u002F 5\n",[22,72455,72456,72478],{"__ignoreMap":133},[137,72457,72458,72460,72462,72464,72466,72468,72470,72472,72474,72476],{"class":139,"line":140},[137,72459,3077],{"class":143},[137,72461,26939],{"class":364},[137,72463,151],{"class":143},[137,72465,15069],{"class":143},[137,72467,17266],{"class":147},[137,72469,356],{"class":157},[137,72471,10345],{"class":364},[137,72473,164],{"class":157},[137,72475,60501],{"class":364},[137,72477,1502],{"class":157},[137,72479,72480,72482,72484,72487],{"class":139,"line":173},[137,72481,1436],{"class":157},[137,72483,353],{"class":147},[137,72485,72486],{"class":157},"(result); ",[137,72488,72489],{"class":308},"\u002F\u002F 5\n",[27,72491,72492],{},"Simple, right? That's what RPC looks like. But before we dive into the code, let's explore how we got here.",[27,72494,72495],{},[63,72496],{"alt":72434,"src":72497},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1768564822\u002Fblog\u002Funderstanding-modern-rpc-frameworks-how-they-work-and-when-to-use-them\u002Ftheres-another-way-rpc_aoirtg.jpg",[104,72499,72501],{"id":72500},"rpc-isnt-new","RPC Isn't New",[27,72503,72504],{},"Here's something surprising: RPC is actually older than REST.",[27,72506,72507,72508,114,72511,72514],{},"The idea was formalised in 1984 by ",[30,72509,72510],{},"Andrew Birrell",[30,72512,72513],{},"Bruce Jay Nelson"," at Xerox PARC. Their goal was ambitious yet simple: make calling a function on another machine feel exactly like calling a local function.",[27,72516,72517],{},"So yes - RPC has been around since the 1980s. But why are we hearing more about it lately?",[104,72519,72521],{"id":72520},"rpc-through-the-decades",[42,72522,72523],{},"RPC Through the Decades",[27,72525,72526],{},"RPC has evolved through several distinct eras.",[1003,72528,72529,72556,72578],{},[1006,72530,72531,72534],{},[42,72532,72533],{},"Late 1980s–1990s: Enterprise RPC",[1003,72535,72536],{},[1006,72537,72538,164,72543,164,72548,72553],{},[45,72539,72542],{"href":72540,"target":2716,"rel":72541},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSun_RPC",[2718,2719],"Sun RPC",[45,72544,72547],{"href":72545,"target":2716,"rel":72546},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FDCE\u002FRPC",[2718,2719],"DCE\u002FRPC",[45,72549,72552],{"href":72550,"target":2716,"rel":72551},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCommon_Object_Request_Broker_Architecture",[2718,2719],"CORBA",[30,72554,72555],{},"Used heavily in large, distributed enterprise systems",[1006,72557,72558,72561],{},[42,72559,72560],{},"Early 2000s: Web-Friendly RPC",[1003,72562,72563],{},[1006,72564,72565,164,72570,72575],{},[45,72566,72569],{"href":72567,"target":2716,"rel":72568},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FXML-RPC",[2718,2719],"XML-RPC",[45,72571,72574],{"href":72572,"target":2716,"rel":72573},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSOAP",[2718,2719],"SOAP",[30,72576,72577],{},"Widely adopted, but verbose and heavyweight",[1006,72579,72580,72583],{},[42,72581,72582],{},"2010s–today: Lightweight & Fast",[1003,72584,72585,72593],{},[1006,72586,72587,72592],{},[45,72588,72591],{"href":72589,"target":2716,"rel":72590},"https:\u002F\u002Fwww.jsonrpc.org\u002F",[2718,2719],"JSON-RPC"," (simple and readable)",[1006,72594,72595,72596,164,72601,14528,72606,72611],{},"Modern frameworks like ",[45,72597,72600],{"href":72598,"target":2716,"rel":72599},"https:\u002F\u002Fgrpc.io\u002F",[2718,2719],"gRPC",[45,72602,72605],{"href":72603,"target":2716,"rel":72604},"https:\u002F\u002Ftrpc.io\u002F",[2718,2719],"tRPC",[45,72607,72610],{"href":72608,"target":2716,"rel":72609},"https:\u002F\u002Forpc.dev\u002F",[2718,2719],"oRPC"," with end-to-end type safety",[27,72613,72614],{},"Early RPC systems were complex. Technologies like SOAP and XML-RPC were powerful but heavy and difficult to work with.",[27,72616,72617],{},"In the 2010s, things changed. Modern frameworks like gRPC, tRPC, and oRPC emerged - lightweight, fast, and easier to implement. They delivered speed with simple integration and, importantly, automatic type checking between client and server.",[27,72619,72620],{},"This made a big difference. Better tools made RPC easier to use, leading to wider adoption.",[104,72622,72624],{"id":72623},"why-rpc-is-growing-in-popularity","Why RPC Is Growing in Popularity",[27,72626,72627],{},"Over the past decade, web application architecture has changed dramatically. As teams work with larger monorepos and distributed microservices, end-to-end type safety has become essential. This shift naturally led many teams toward RPC-based frameworks like gRPC, tRPC, and oRPC.",[27,72629,72630],{},"These tools bridge the gap between client and server. Instead of treating the backend as a distant HTTP interface, you call remote procedures like local functions. This eliminates the \"parallel universes\" problem where client and server APIs slowly drift out of sync.",[27,72632,72633],{},"RPC frameworks strike a practical balance - they combine the performance and reliability of strongly typed contracts with the simplicity of direct function calls.",[27,72635,72636],{},"gRPC approaches modern RPC from an infrastructure perspective. By defining services using Protocol Buffers, it enforces strong, language-agnostic contracts and enables fast, reliable communication between services. This makes gRPC ideal for internal microservices where performance, strict schemas, and cross-language support are critical.",[27,72638,72639],{},"tRPC takes a different approach. By leveraging TypeScript's type inference, the client consumes the server's router types directly. The result: rich autocompletion, compile-time validation, and a smooth developer experience.",[27,72641,72642],{},"oRPC offers a similar experience while staying compatible with OpenAPI. This makes it easier to expose RPC-style APIs to non-TypeScript clients and third-party integrations without sacrificing type safety.",[104,72644,72646],{"id":72645},"rpc-in-the-ai-era","RPC in the AI Era",[27,72648,72649],{},"RPC has also found renewed relevance in the AI era. AI systems don't think in terms of resources - they think in terms of actions.",[27,72651,72652,72653,164,72656,164,72659,164,72662,14528,72665,72668],{},"Modern AI agents don't just fetch data, they execute capabilities. They ",[30,72654,72655],{},"search",[30,72657,72658],{},"calculate",[30,72660,72661],{},"read files",[30,72663,72664],{},"call tools",[30,72666,72667],{},"chain decisions together",". This interaction model maps naturally to function invocation.",[27,72670,72671,72672,72677,72678,72680],{},"A clear example is the ",[45,72673,72676],{"href":72674,"target":2716,"rel":72675},"https:\u002F\u002Fmodelcontextprotocol.io\u002Fdocs\u002Fgetting-started\u002Fintro",[2718,2719],"Model Context Protocol"," (MCP) ",[42,72679,8215],{}," an open standard introduced in late 2024 to connect large language models with tools, data sources, and execution environments. Under the hood, MCP is fundamentally RPC-based, using JSON-RPC 2.0 as its core communication layer.",[27,72682,72683],{},"Instead of exposing resources, MCP exposes procedures:",[1003,72685,72686,72691,72696,72701],{},[1006,72687,72688],{},[22,72689,72690],{},"tools\u002Fcall",[1006,72692,72693],{},[22,72694,72695],{},"resources\u002Fread",[1006,72697,72698],{},[22,72699,72700],{},"prompts\u002Fget",[1006,72702,72703],{},[22,72704,72705],{},"sampling\u002FcreateMessage",[27,72707,72708],{},"Each interaction is a structured remote function call with well-defined inputs, outputs, and error handling. This lets an AI agent treat external systems as extensions of its own runtime - much closer to calling a local function than issuing an HTTP request.",[27,72710,72711],{},"This pattern is becoming common across AI infrastructure:",[1003,72713,72714,72720,72726,72732],{},[1006,72715,72716,72719],{},[42,72717,72718],{},"Tool calling"," is RPC by design - invoke a method with parameters, receive a result",[1006,72721,72722,72725],{},[42,72723,72724],{},"Agent orchestration"," relies on asynchronous, correlated procedure calls",[1006,72727,72728,72731],{},[42,72729,72730],{},"Context retrieval"," maps cleanly to callable capabilities",[1006,72733,72734,72737],{},[42,72735,72736],{},"Bidirectional workflows"," (where servers can request model outputs) require RPC-style messaging",[27,72739,72740,72741,72744],{},"RPC is becoming the ",[42,72742,72743],{},"\"system call layer\" for AI",". Just as operating systems expose functions for applications to interact with hardware, AI platforms expose RPC interfaces for models to interact with the world.",[104,72746,72748],{"id":72747},"understanding-rpc-through-examples","Understanding RPC Through Examples",[27,72750,72751],{},"Now that we understand what RPC is and why it matters, let's explore it by building the same simple example in different ways. We'll use the same function throughout:",[27,72753,72754],{},[22,72755,72756],{},"add(2, 3) → 5",[27,72758,72759],{},"Nothing fancy - just enough to see how each approach works and what trade-offs it makes.",[27,72761,72762],{},"We'll start with the most basic RPC implementation and work our way up to modern, type-safe solutions.",[104,72764,72766],{"id":72765},"plain-rpc-no-standard-no-framework","Plain RPC (No Standard, No Framework)",[27,72768,72769],{},"Let's start with the simplest possible RPC implementation. This is RPC in its purest form - no libraries, no standards, just the core idea.",[123,72771,72772],{"id":36647},"How It Works",[27,72774,72775],{},"The flow is straightforward:",[2569,72777,72778,72781,72784,72787],{},[1006,72779,72780],{},"We send the name of the function we want to call",[1006,72782,72783],{},"We send the parameters for that function",[1006,72785,72786],{},"The server finds the function and runs it",[1006,72788,72789],{},"The server sends back the result",[27,72791,72792],{},"Let's see what this looks like in code.",[123,72794,72796],{"id":72795},"plain-rpc-server","Plain RPC Server",[128,72798,72800],{"className":13299,"code":72799,"language":13301,"meta":133,"style":133},"import http from \"http\";\n\nconst methods: Record\u003Cstring, (...args: number[]) => number> = {\n    add: (a: number, b: number) => a + b,\n};\n\nhttp.createServer((req, res) => {\n    let body = \"\";\n\n    req.on(\"data\", (chunk) => (body += chunk));\n    req.on(\"end\", () => {\n        const { method, params } = JSON.parse(body);\n        const result = methods[method](...params);\n\n        res.end(JSON.stringify({ result }));\n    });\n}).listen(3000, () => {\n    console.log(\"Plain RPC server running on http:\u002F\u002Flocalhost:3000\");\n});\n",[22,72801,72802,72814,72818,72858,72890,72894,72898,72918,72930,72934,72959,72975,72999,73015,73019,73036,73040,73056,73069],{"__ignoreMap":133},[137,72803,72804,72806,72808,72810,72812],{"class":139,"line":140},[137,72805,10287],{"class":143},[137,72807,72035],{"class":157},[137,72809,10954],{"class":143},[137,72811,72040],{"class":284},[137,72813,3276],{"class":157},[137,72815,72816],{"class":139,"line":173},[137,72817,516],{"emptyLinePlaceholder":515},[137,72819,72820,72822,72825,72827,72830,72832,72834,72836,72838,72841,72843,72845,72848,72850,72852,72854,72856],{"class":139,"line":188},[137,72821,3077],{"class":143},[137,72823,72824],{"class":364}," methods",[137,72826,894],{"class":143},[137,72828,72829],{"class":147}," Record",[137,72831,4033],{"class":157},[137,72833,14158],{"class":364},[137,72835,24531],{"class":157},[137,72837,14408],{"class":143},[137,72839,72840],{"class":161},"args",[137,72842,894],{"class":143},[137,72844,31395],{"class":364},[137,72846,72847],{"class":157},"[]) ",[137,72849,222],{"class":143},[137,72851,31395],{"class":364},[137,72853,14124],{"class":157},[137,72855,253],{"class":143},[137,72857,256],{"class":157},[137,72859,72860,72863,72865,72867,72869,72871,72873,72875,72877,72879,72881,72883,72885,72887],{"class":139,"line":269},[137,72861,72862],{"class":147},"    add",[137,72864,51456],{"class":157},[137,72866,45],{"class":161},[137,72868,894],{"class":143},[137,72870,31395],{"class":364},[137,72872,164],{"class":157},[137,72874,167],{"class":161},[137,72876,894],{"class":143},[137,72878,31395],{"class":364},[137,72880,219],{"class":157},[137,72882,222],{"class":143},[137,72884,179],{"class":157},[137,72886,182],{"class":143},[137,72888,72889],{"class":157}," b,\n",[137,72891,72892],{"class":139,"line":278},[137,72893,191],{"class":157},[137,72895,72896],{"class":139,"line":291},[137,72897,516],{"emptyLinePlaceholder":515},[137,72899,72900,72902,72904,72906,72908,72910,72912,72914,72916],{"class":139,"line":297},[137,72901,72051],{"class":157},[137,72903,72054],{"class":147},[137,72905,2774],{"class":157},[137,72907,9133],{"class":161},[137,72909,164],{"class":157},[137,72911,9138],{"class":161},[137,72913,219],{"class":157},[137,72915,222],{"class":143},[137,72917,256],{"class":157},[137,72919,72920,72922,72924,72926,72928],{"class":139,"line":302},[137,72921,58054],{"class":143},[137,72923,72099],{"class":157},[137,72925,253],{"class":143},[137,72927,4607],{"class":284},[137,72929,3276],{"class":157},[137,72931,72932],{"class":139,"line":662},[137,72933,516],{"emptyLinePlaceholder":515},[137,72935,72936,72939,72941,72943,72945,72947,72949,72951,72953,72955,72957],{"class":139,"line":667},[137,72937,72938],{"class":157},"    req.",[137,72940,72117],{"class":147},[137,72942,356],{"class":157},[137,72944,38270],{"class":284},[137,72946,24531],{"class":157},[137,72948,72126],{"class":161},[137,72950,219],{"class":157},[137,72952,222],{"class":143},[137,72954,72133],{"class":157},[137,72956,72136],{"class":143},[137,72958,72139],{"class":157},[137,72960,72961,72963,72965,72967,72969,72971,72973],{"class":139,"line":786},[137,72962,72938],{"class":157},[137,72964,72117],{"class":147},[137,72966,356],{"class":157},[137,72968,72150],{"class":284},[137,72970,4420],{"class":157},[137,72972,222],{"class":143},[137,72974,256],{"class":157},[137,72976,72977,72979,72981,72983,72985,72987,72989,72991,72993,72995,72997],{"class":139,"line":798},[137,72978,3008],{"class":143},[137,72980,8906],{"class":157},[137,72982,5364],{"class":364},[137,72984,164],{"class":157},[137,72986,61925],{"class":364},[137,72988,8911],{"class":157},[137,72990,253],{"class":143},[137,72992,17436],{"class":364},[137,72994,1017],{"class":157},[137,72996,17441],{"class":147},[137,72998,32215],{"class":157},[137,73000,73001,73003,73005,73007,73010,73012],{"class":139,"line":803},[137,73002,3008],{"class":143},[137,73004,26939],{"class":364},[137,73006,151],{"class":143},[137,73008,73009],{"class":157}," methods[method](",[137,73011,14408],{"class":143},[137,73013,73014],{"class":157},"params);\n",[137,73016,73017],{"class":139,"line":931},[137,73018,516],{"emptyLinePlaceholder":515},[137,73020,73021,73023,73025,73027,73029,73031,73033],{"class":139,"line":1568},[137,73022,33014],{"class":157},[137,73024,33017],{"class":147},[137,73026,356],{"class":157},[137,73028,22554],{"class":364},[137,73030,1017],{"class":157},[137,73032,24816],{"class":147},[137,73034,73035],{"class":157},"({ result }));\n",[137,73037,73038],{"class":139,"line":1573},[137,73039,2832],{"class":157},[137,73041,73042,73044,73046,73048,73050,73052,73054],{"class":139,"line":1578},[137,73043,72216],{"class":157},[137,73045,15106],{"class":147},[137,73047,356],{"class":157},[137,73049,15111],{"class":364},[137,73051,4420],{"class":157},[137,73053,222],{"class":143},[137,73055,256],{"class":157},[137,73057,73058,73060,73062,73064,73067],{"class":139,"line":1588},[137,73059,493],{"class":157},[137,73061,353],{"class":147},[137,73063,356],{"class":157},[137,73065,73066],{"class":284},"\"Plain RPC server running on http:\u002F\u002Flocalhost:3000\"",[137,73068,1502],{"class":157},[137,73070,73071],{"class":139,"line":1601},[137,73072,5422],{"class":157},[27,73074,73075],{},"The server is refreshingly simple. It maintains a registry of available functions and when a request comes in, it looks up the function by name and executes it with the provided parameters.",[123,73077,73079],{"id":73078},"plain-rpc-client","Plain RPC Client",[128,73081,73083],{"className":13299,"code":73082,"language":13301,"meta":133,"style":133},"const callRpc = async () => {\n    const res = await fetch(\"http:\u002F\u002Flocalhost:3000\", {\n        method: \"POST\",\n        body: JSON.stringify({\n            method: \"add\",\n            params: [2, 3],\n        }),\n    });\n\n    const data = await res.json();\n    console.log(\"Result:\", data.result);\n};\n\ncallRpc();\n",[22,73084,73085,73102,73121,73130,73143,73153,73166,73170,73174,73178,73194,73207,73211,73215],{"__ignoreMap":133},[137,73086,73087,73089,73092,73094,73096,73098,73100],{"class":139,"line":140},[137,73088,3077],{"class":143},[137,73090,73091],{"class":147}," callRpc",[137,73093,151],{"class":143},[137,73095,32008],{"class":143},[137,73097,1484],{"class":157},[137,73099,222],{"class":143},[137,73101,256],{"class":157},[137,73103,73104,73106,73108,73110,73112,73114,73116,73119],{"class":139,"line":173},[137,73105,4177],{"class":143},[137,73107,32992],{"class":364},[137,73109,151],{"class":143},[137,73111,15069],{"class":143},[137,73113,68951],{"class":147},[137,73115,356],{"class":157},[137,73117,73118],{"class":284},"\"http:\u002F\u002Flocalhost:3000\"",[137,73120,5396],{"class":157},[137,73122,73123,73126,73128],{"class":139,"line":188},[137,73124,73125],{"class":157},"        method: ",[137,73127,68971],{"class":284},[137,73129,1961],{"class":157},[137,73131,73132,73135,73137,73139,73141],{"class":139,"line":269},[137,73133,73134],{"class":157},"        body: ",[137,73136,22554],{"class":364},[137,73138,1017],{"class":157},[137,73140,24816],{"class":147},[137,73142,3175],{"class":157},[137,73144,73145,73148,73151],{"class":139,"line":278},[137,73146,73147],{"class":157},"            method: ",[137,73149,73150],{"class":284},"\"add\"",[137,73152,1961],{"class":157},[137,73154,73155,73158,73160,73162,73164],{"class":139,"line":291},[137,73156,73157],{"class":157},"            params: [",[137,73159,10345],{"class":364},[137,73161,164],{"class":157},[137,73163,60501],{"class":364},[137,73165,21916],{"class":157},[137,73167,73168],{"class":139,"line":297},[137,73169,13426],{"class":157},[137,73171,73172],{"class":139,"line":302},[137,73173,2832],{"class":157},[137,73175,73176],{"class":139,"line":662},[137,73177,516],{"emptyLinePlaceholder":515},[137,73179,73180,73182,73184,73186,73188,73190,73192],{"class":139,"line":667},[137,73181,4177],{"class":143},[137,73183,38067],{"class":364},[137,73185,151],{"class":143},[137,73187,15069],{"class":143},[137,73189,72318],{"class":157},[137,73191,5157],{"class":147},[137,73193,924],{"class":157},[137,73195,73196,73198,73200,73202,73204],{"class":139,"line":786},[137,73197,493],{"class":157},[137,73199,353],{"class":147},[137,73201,356],{"class":157},[137,73203,72345],{"class":284},[137,73205,73206],{"class":157},", data.result);\n",[137,73208,73209],{"class":139,"line":798},[137,73210,191],{"class":157},[137,73212,73213],{"class":139,"line":803},[137,73214,516],{"emptyLinePlaceholder":515},[137,73216,73217,73220],{"class":139,"line":931},[137,73218,73219],{"class":147},"callRpc",[137,73221,924],{"class":157},[27,73223,73224],{},"On the client side, we're just sending a JSON object with two fields: which method to call, and what parameters to pass. It's almost like calling a function, but over HTTP.",[123,73226,73228],{"id":73227},"whats-good-about-this","What's Good About This",[1003,73230,73231,73234,73237],{},[1006,73232,73233],{},"It actually works! You can run this code right now",[1006,73235,73236],{},"It's easy to understand - there's no magic happening here",[1006,73238,73239],{},"Zero dependencies - just plain Node.js",[123,73241,73243],{"id":73242},"whats-not-so-good","What's Not So Good",[1003,73245,73246,73249,73252,73255],{},[1006,73247,73248],{},"No rules or standards - everyone could implement this differently",[1006,73250,73251],{},"No error handling - what happens when something goes wrong?",[1006,73253,73254],{},"No contracts - the client and server just have to \"know\" what methods exist and hope they agree",[1006,73256,73257],{},"If someone calls a method that doesn't exist... your app crashes",[27,73259,73260],{},"This implementation shows you the essence of RPC. It's raw and unpolished, but you can see the core concept clearly: calling a remote function almost like it's local.",[27,73262,73263],{},"Of course, you wouldn't want to use this in a real application. That's where standards and frameworks come in - which we'll explore next.",[104,73265,73267],{"id":73266},"json-rpc-same-idea-standardised","JSON-RPC (Same Idea, Standardised)",[27,73269,73270],{},"JSON-RPC takes the plain RPC idea we just saw and wraps it in a formal specification. It was first introduced around 2005 and has evolved through several versions, with JSON-RPC 2.0 being the current standard.",[27,73272,73273],{},"The core concept is identical - call a remote function by name and pass parameters - but now there's structure around it.",[123,73275,73277],{"id":73276},"what-json-rpc-adds","What JSON-RPC Adds",[1003,73279,73280,73286,73289,73292],{},[1006,73281,73282,73283,14105],{},"A version identifier (",[22,73284,73285],{},"jsonrpc: \"2.0\"",[1006,73287,73288],{},"A standard request\u002Fresponse format",[1006,73290,73291],{},"Error handling conventions",[1006,73293,73294],{},"Request IDs for matching responses (useful for batch requests)",[27,73296,73297],{},"Let's see what this looks like in practice.",[123,73299,73301],{"id":73300},"json-rpc-request-format","JSON-RPC Request Format",[128,73303,73305],{"className":5155,"code":73304,"language":5157,"meta":133,"style":133},"{\n    \"jsonrpc\": \"2.0\",\n    \"method\": \"add\",\n    \"params\": [2, 3],\n    \"id\": 1\n}\n",[22,73306,73307,73311,73323,73334,73349,73359],{"__ignoreMap":133},[137,73308,73309],{"class":139,"line":140},[137,73310,15971],{"class":157},[137,73312,73313,73316,73318,73321],{"class":139,"line":173},[137,73314,73315],{"class":364},"    \"jsonrpc\"",[137,73317,726],{"class":157},[137,73319,73320],{"class":284},"\"2.0\"",[137,73322,1961],{"class":157},[137,73324,73325,73328,73330,73332],{"class":139,"line":188},[137,73326,73327],{"class":364},"    \"method\"",[137,73329,726],{"class":157},[137,73331,73150],{"class":284},[137,73333,1961],{"class":157},[137,73335,73336,73339,73341,73343,73345,73347],{"class":139,"line":269},[137,73337,73338],{"class":364},"    \"params\"",[137,73340,29669],{"class":157},[137,73342,10345],{"class":364},[137,73344,164],{"class":157},[137,73346,60501],{"class":364},[137,73348,21916],{"class":157},[137,73350,73351,73354,73356],{"class":139,"line":278},[137,73352,73353],{"class":364},"    \"id\"",[137,73355,726],{"class":157},[137,73357,73358],{"class":364},"1\n",[137,73360,73361],{"class":139,"line":291},[137,73362,510],{"class":157},[27,73364,73365,73366,73368],{},"Notice the structure. We're calling the same ",[22,73367,34393],{}," function, but now it's wrapped in a standardised format that any JSON-RPC implementation can understand.",[123,73370,73372],{"id":73371},"json-rpc-response-format","JSON-RPC Response Format",[128,73374,73376],{"className":5155,"code":73375,"language":5157,"meta":133,"style":133},"{\n    \"jsonrpc\": \"2.0\",\n    \"result\": 5,\n    \"id\": 1\n}\n",[22,73377,73378,73382,73392,73404,73412],{"__ignoreMap":133},[137,73379,73380],{"class":139,"line":140},[137,73381,15971],{"class":157},[137,73383,73384,73386,73388,73390],{"class":139,"line":173},[137,73385,73315],{"class":364},[137,73387,726],{"class":157},[137,73389,73320],{"class":284},[137,73391,1961],{"class":157},[137,73393,73394,73397,73399,73402],{"class":139,"line":188},[137,73395,73396],{"class":364},"    \"result\"",[137,73398,726],{"class":157},[137,73400,73401],{"class":364},"5",[137,73403,1961],{"class":157},[137,73405,73406,73408,73410],{"class":139,"line":269},[137,73407,73353],{"class":364},[137,73409,726],{"class":157},[137,73411,73358],{"class":364},[137,73413,73414],{"class":139,"line":278},[137,73415,510],{"class":157},[27,73417,73418],{},"The response follows the same pattern - clean, predictable, and easy to parse.",[123,73420,73422],{"id":73421},"json-rpc-server","JSON-RPC Server",[128,73424,73426],{"className":13299,"code":73425,"language":13301,"meta":133,"style":133},"import http from \"http\";\n\nconst methods: Record\u003Cstring, (...args: number[]) => number> = {\n    add: (a: number, b: number) => a + b,\n};\n\nhttp.createServer((req, res) => {\n    let body = \"\";\n\n    req.on(\"data\", (chunk) => (body += chunk));\n    req.on(\"end\", () => {\n        const { method, params, id } = JSON.parse(body);\n\n        const result = methods[method](...params);\n\n        res.end(\n            JSON.stringify({\n                jsonrpc: \"2.0\",\n                result,\n                id,\n            })\n        );\n    });\n}).listen(3000, () => {\n    console.log(\"JSON-RPC server running on http:\u002F\u002Flocalhost:3000\");\n});\n",[22,73427,73428,73440,73444,73480,73510,73514,73518,73538,73550,73554,73578,73594,73622,73626,73640,73644,73652,73663,73672,73677,73682,73686,73690,73694,73710,73723],{"__ignoreMap":133},[137,73429,73430,73432,73434,73436,73438],{"class":139,"line":140},[137,73431,10287],{"class":143},[137,73433,72035],{"class":157},[137,73435,10954],{"class":143},[137,73437,72040],{"class":284},[137,73439,3276],{"class":157},[137,73441,73442],{"class":139,"line":173},[137,73443,516],{"emptyLinePlaceholder":515},[137,73445,73446,73448,73450,73452,73454,73456,73458,73460,73462,73464,73466,73468,73470,73472,73474,73476,73478],{"class":139,"line":188},[137,73447,3077],{"class":143},[137,73449,72824],{"class":364},[137,73451,894],{"class":143},[137,73453,72829],{"class":147},[137,73455,4033],{"class":157},[137,73457,14158],{"class":364},[137,73459,24531],{"class":157},[137,73461,14408],{"class":143},[137,73463,72840],{"class":161},[137,73465,894],{"class":143},[137,73467,31395],{"class":364},[137,73469,72847],{"class":157},[137,73471,222],{"class":143},[137,73473,31395],{"class":364},[137,73475,14124],{"class":157},[137,73477,253],{"class":143},[137,73479,256],{"class":157},[137,73481,73482,73484,73486,73488,73490,73492,73494,73496,73498,73500,73502,73504,73506,73508],{"class":139,"line":269},[137,73483,72862],{"class":147},[137,73485,51456],{"class":157},[137,73487,45],{"class":161},[137,73489,894],{"class":143},[137,73491,31395],{"class":364},[137,73493,164],{"class":157},[137,73495,167],{"class":161},[137,73497,894],{"class":143},[137,73499,31395],{"class":364},[137,73501,219],{"class":157},[137,73503,222],{"class":143},[137,73505,179],{"class":157},[137,73507,182],{"class":143},[137,73509,72889],{"class":157},[137,73511,73512],{"class":139,"line":278},[137,73513,191],{"class":157},[137,73515,73516],{"class":139,"line":291},[137,73517,516],{"emptyLinePlaceholder":515},[137,73519,73520,73522,73524,73526,73528,73530,73532,73534,73536],{"class":139,"line":297},[137,73521,72051],{"class":157},[137,73523,72054],{"class":147},[137,73525,2774],{"class":157},[137,73527,9133],{"class":161},[137,73529,164],{"class":157},[137,73531,9138],{"class":161},[137,73533,219],{"class":157},[137,73535,222],{"class":143},[137,73537,256],{"class":157},[137,73539,73540,73542,73544,73546,73548],{"class":139,"line":302},[137,73541,58054],{"class":143},[137,73543,72099],{"class":157},[137,73545,253],{"class":143},[137,73547,4607],{"class":284},[137,73549,3276],{"class":157},[137,73551,73552],{"class":139,"line":662},[137,73553,516],{"emptyLinePlaceholder":515},[137,73555,73556,73558,73560,73562,73564,73566,73568,73570,73572,73574,73576],{"class":139,"line":667},[137,73557,72938],{"class":157},[137,73559,72117],{"class":147},[137,73561,356],{"class":157},[137,73563,38270],{"class":284},[137,73565,24531],{"class":157},[137,73567,72126],{"class":161},[137,73569,219],{"class":157},[137,73571,222],{"class":143},[137,73573,72133],{"class":157},[137,73575,72136],{"class":143},[137,73577,72139],{"class":157},[137,73579,73580,73582,73584,73586,73588,73590,73592],{"class":139,"line":786},[137,73581,72938],{"class":157},[137,73583,72117],{"class":147},[137,73585,356],{"class":157},[137,73587,72150],{"class":284},[137,73589,4420],{"class":157},[137,73591,222],{"class":143},[137,73593,256],{"class":157},[137,73595,73596,73598,73600,73602,73604,73606,73608,73610,73612,73614,73616,73618,73620],{"class":139,"line":798},[137,73597,3008],{"class":143},[137,73599,8906],{"class":157},[137,73601,5364],{"class":364},[137,73603,164],{"class":157},[137,73605,61925],{"class":364},[137,73607,164],{"class":157},[137,73609,31478],{"class":364},[137,73611,8911],{"class":157},[137,73613,253],{"class":143},[137,73615,17436],{"class":364},[137,73617,1017],{"class":157},[137,73619,17441],{"class":147},[137,73621,32215],{"class":157},[137,73623,73624],{"class":139,"line":803},[137,73625,516],{"emptyLinePlaceholder":515},[137,73627,73628,73630,73632,73634,73636,73638],{"class":139,"line":931},[137,73629,3008],{"class":143},[137,73631,26939],{"class":364},[137,73633,151],{"class":143},[137,73635,73009],{"class":157},[137,73637,14408],{"class":143},[137,73639,73014],{"class":157},[137,73641,73642],{"class":139,"line":1568},[137,73643,516],{"emptyLinePlaceholder":515},[137,73645,73646,73648,73650],{"class":139,"line":1573},[137,73647,33014],{"class":157},[137,73649,33017],{"class":147},[137,73651,11813],{"class":157},[137,73653,73654,73657,73659,73661],{"class":139,"line":1578},[137,73655,73656],{"class":364},"            JSON",[137,73658,1017],{"class":157},[137,73660,24816],{"class":147},[137,73662,3175],{"class":157},[137,73664,73665,73668,73670],{"class":139,"line":1588},[137,73666,73667],{"class":157},"                jsonrpc: ",[137,73669,73320],{"class":284},[137,73671,1961],{"class":157},[137,73673,73674],{"class":139,"line":1601},[137,73675,73676],{"class":157},"                result,\n",[137,73678,73679],{"class":139,"line":3802},[137,73680,73681],{"class":157},"                id,\n",[137,73683,73684],{"class":139,"line":3808},[137,73685,14293],{"class":157},[137,73687,73688],{"class":139,"line":3822},[137,73689,49105],{"class":157},[137,73691,73692],{"class":139,"line":3827},[137,73693,2832],{"class":157},[137,73695,73696,73698,73700,73702,73704,73706,73708],{"class":139,"line":3832},[137,73697,72216],{"class":157},[137,73699,15106],{"class":147},[137,73701,356],{"class":157},[137,73703,15111],{"class":364},[137,73705,4420],{"class":157},[137,73707,222],{"class":143},[137,73709,256],{"class":157},[137,73711,73712,73714,73716,73718,73721],{"class":139,"line":3840},[137,73713,493],{"class":157},[137,73715,353],{"class":147},[137,73717,356],{"class":157},[137,73719,73720],{"class":284},"\"JSON-RPC server running on http:\u002F\u002Flocalhost:3000\"",[137,73722,1502],{"class":157},[137,73724,73725],{"class":139,"line":3846},[137,73726,5422],{"class":157},[27,73728,73729],{},"The server implementation looks almost identical to our plain RPC version. The main difference is that we're now following the JSON-RPC 2.0 format - adding the version identifier and including the request ID in our response.",[123,73731,73733],{"id":73732},"json-rpc-client","JSON-RPC Client",[128,73735,73737],{"className":13299,"code":73736,"language":13301,"meta":133,"style":133},"fetch(\"http:\u002F\u002Flocalhost:3000\", {\n    method: \"POST\",\n    body: JSON.stringify({\n        jsonrpc: \"2.0\",\n        method: \"add\",\n        params: [2, 3],\n        id: 1,\n    }),\n})\n    .then((res) => res.json())\n    .then((data) => console.log(\"Result:\", data.result));\n",[22,73738,73739,73749,73757,73769,73778,73786,73799,73808,73813,73817,73837],{"__ignoreMap":133},[137,73740,73741,73743,73745,73747],{"class":139,"line":140},[137,73742,59737],{"class":147},[137,73744,356],{"class":157},[137,73746,73118],{"class":284},[137,73748,5396],{"class":157},[137,73750,73751,73753,73755],{"class":139,"line":173},[137,73752,72269],{"class":157},[137,73754,68971],{"class":284},[137,73756,1961],{"class":157},[137,73758,73759,73761,73763,73765,73767],{"class":139,"line":188},[137,73760,72278],{"class":157},[137,73762,22554],{"class":364},[137,73764,1017],{"class":157},[137,73766,24816],{"class":147},[137,73768,3175],{"class":157},[137,73770,73771,73774,73776],{"class":139,"line":269},[137,73772,73773],{"class":157},"        jsonrpc: ",[137,73775,73320],{"class":284},[137,73777,1961],{"class":157},[137,73779,73780,73782,73784],{"class":139,"line":278},[137,73781,73125],{"class":157},[137,73783,73150],{"class":284},[137,73785,1961],{"class":157},[137,73787,73788,73791,73793,73795,73797],{"class":139,"line":291},[137,73789,73790],{"class":157},"        params: [",[137,73792,10345],{"class":364},[137,73794,164],{"class":157},[137,73796,60501],{"class":364},[137,73798,21916],{"class":157},[137,73800,73801,73804,73806],{"class":139,"line":297},[137,73802,73803],{"class":157},"        id: ",[137,73805,6065],{"class":364},[137,73807,1961],{"class":157},[137,73809,73810],{"class":139,"line":302},[137,73811,73812],{"class":157},"    }),\n",[137,73814,73815],{"class":139,"line":662},[137,73816,13451],{"class":157},[137,73818,73819,73821,73823,73825,73827,73829,73831,73833,73835],{"class":139,"line":667},[137,73820,2748],{"class":157},[137,73822,2771],{"class":147},[137,73824,2774],{"class":157},[137,73826,9138],{"class":161},[137,73828,219],{"class":157},[137,73830,222],{"class":143},[137,73832,72318],{"class":157},[137,73834,5157],{"class":147},[137,73836,59766],{"class":157},[137,73838,73839,73841,73843,73845,73847,73849,73851,73853,73855,73857,73859],{"class":139,"line":786},[137,73840,2748],{"class":157},[137,73842,2771],{"class":147},[137,73844,2774],{"class":157},[137,73846,1942],{"class":161},[137,73848,219],{"class":157},[137,73850,222],{"class":143},[137,73852,43656],{"class":157},[137,73854,353],{"class":147},[137,73856,356],{"class":157},[137,73858,72345],{"class":284},[137,73860,72348],{"class":157},[27,73862,73863],{},"The client also follows the standard format. We're sending the version, method name, parameters, and an ID to track the request.",[123,73865,73228],{"id":73866},"whats-good-about-this-1",[1003,73868,73869,73872,73875,73878],{},[1006,73870,73871],{},"Everyone speaks the same language - any JSON-RPC client can talk to any JSON-RPC server",[1006,73873,73874],{},"Tooling and libraries are readily available across languages",[1006,73876,73877],{},"Error handling is standardized and predictable",[1006,73879,73880],{},"Batch requests are supported out of the box",[123,73882,73243],{"id":73883},"whats-not-so-good-1",[1003,73885,73886,73892,73895,73898],{},[1006,73887,73888,73889],{},"Still no type safety - nothing stops you from calling ",[22,73890,73891],{},"add(\"hello\", \"world\")",[1006,73893,73894],{},"You'll only discover type mismatches at runtime",[1006,73896,73897],{},"No automatic client generation from server code",[1006,73899,73900],{},"You're still manually constructing requests and parsing responses",[27,73902,73903],{},"JSON-RPC represents a significant step forward. It's simple enough to implement by hand but structured enough to be reliable and interoperable. It's been widely adopted and powers many production systems.",[27,73905,73906],{},"But there's still something missing: type safety. Modern development workflows expect the compiler to catch mistakes before runtime. That's where the next generation of RPC frameworks comes in.",[104,73908,73910],{"id":73909},"modern-rpc-frameworks","Modern RPC Frameworks",[27,73912,73913],{},"We've explored RPC in its foundational forms - plain RPC and JSON-RPC. Both demonstrate the core idea: calling remote functions by name and passing parameters over the network.",[27,73915,73916,73917,73920],{},"Modern tools take a different approach. Instead of treating RPC as just a message format, they treat it as a ",[42,73918,73919],{},"contract-first or type-first system",". Client and server evolve together safely, catching mistakes at build time rather than in production.",[27,73922,73923],{},"This new generation falls into two categories:",[1003,73925,73926,73932],{},[1006,73927,73928,73931],{},[42,73929,73930],{},"Infrastructure-first RPC -"," optimised for performance and cross-language systems (like gRPC)",[1006,73933,73934,73937],{},[42,73935,73936],{},"Developer-experience-first RPC -"," optimised for full-stack productivity and type safety (like tRPC and oRPC)",[27,73939,73940],{},"Let's look at how these tools approach RPC and what trade-offs each one makes.",[104,73942,73944],{"id":73943},"grpc-strong-contracts-high-performance","gRPC (Strong Contracts, High Performance)",[27,73946,73947,73948,34894,73951,73953,73954,9607,73957,73960,73961,73964,73965,73960,73968,73971,73972,1017],{},"gRPC was developed by Google and open-sourced in 2015. Many people assume the \"g\" stands for \"Google,\" but the project has never officially committed to that meaning. Instead, it playfully assigns different expansions with each release - in ",[42,73949,73950],{},"1.1",[30,73952,33931],{}," stood for ",[42,73955,73956],{},"\"good,\"",[42,73958,73959],{},"1.2"," for ",[42,73962,73963],{},"\"green,\""," and in ",[42,73966,73967],{},"1.76",[42,73969,73970],{},"\"genuine.\""," You can see the full list on the gRPC ",[45,73973,31313],{"href":73974,"target":2716,"rel":73975},"https:\u002F\u002Fgrpc.github.io\u002Fgrpc\u002Fcpp\u002Fmd_doc_g_stands_for.html",[2718,2719],[123,73977,73979],{"id":73978},"what-makes-grpc-different","What Makes gRPC Different",[27,73981,73982,73983,73986,73987,73990],{},"gRPC introduces ",[42,73984,73985],{},"contracts"," through Protocol Buffers (protobuf). You define exactly what your service looks like in a ",[22,73988,73989],{},".proto"," file before writing any code. This contract then generates code for both the server and client.",[27,73992,73993],{},"gRPC also uses HTTP\u002F2 and binary serialization, making it significantly faster than JSON-based approaches.",[123,73995,73997],{"id":73996},"the-contract-calculatorproto","The Contract (calculator.proto)",[128,73999,74003],{"className":74000,"code":74001,"language":74002,"meta":133,"style":133},"language-protobuf shiki shiki-themes github-light github-dark","syntax = \"proto3\";\n\nservice Calculator {\n  rpc Add (AddRequest) returns (AddResponse);\n}\n\nmessage AddRequest {\n  int32 a = 1;\n  int32 b = 2;\n}\n\nmessage AddResponse {\n  int32 result = 1;\n}\n","protobuf",[22,74004,74005,74010,74014,74019,74024,74028,74032,74037,74042,74047,74051,74055,74060,74065],{"__ignoreMap":133},[137,74006,74007],{"class":139,"line":140},[137,74008,74009],{},"syntax = \"proto3\";\n",[137,74011,74012],{"class":139,"line":173},[137,74013,516],{"emptyLinePlaceholder":515},[137,74015,74016],{"class":139,"line":188},[137,74017,74018],{},"service Calculator {\n",[137,74020,74021],{"class":139,"line":269},[137,74022,74023],{},"  rpc Add (AddRequest) returns (AddResponse);\n",[137,74025,74026],{"class":139,"line":278},[137,74027,510],{},[137,74029,74030],{"class":139,"line":291},[137,74031,516],{"emptyLinePlaceholder":515},[137,74033,74034],{"class":139,"line":297},[137,74035,74036],{},"message AddRequest {\n",[137,74038,74039],{"class":139,"line":302},[137,74040,74041],{},"  int32 a = 1;\n",[137,74043,74044],{"class":139,"line":662},[137,74045,74046],{},"  int32 b = 2;\n",[137,74048,74049],{"class":139,"line":667},[137,74050,510],{},[137,74052,74053],{"class":139,"line":786},[137,74054,516],{"emptyLinePlaceholder":515},[137,74056,74057],{"class":139,"line":798},[137,74058,74059],{},"message AddResponse {\n",[137,74061,74062],{"class":139,"line":803},[137,74063,74064],{},"  int32 result = 1;\n",[137,74066,74067],{"class":139,"line":931},[137,74068,510],{},[27,74070,74071,74072,74075,74076,74079,74080,74083],{},"This file defines a ",[22,74073,74074],{},"Calculator"," service with an Add method. It takes an ",[22,74077,74078],{},"AddRequest"," (with two integers) and returns an ",[22,74081,74082],{},"AddResponse"," (with one integer).",[123,74085,74087],{"id":74086},"grpc-server","gRPC Server",[128,74089,74091],{"className":13299,"code":74090,"language":13301,"meta":133,"style":133},"import grpc from \"@grpc\u002Fgrpc-js\";\nimport protoLoader from \"@grpc\u002Fproto-loader\";\n\nconst def = protoLoader.loadSync(\"calculator.proto\");\nconst proto = grpc.loadPackageDefinition(def) as any;\n\nconst server = new grpc.Server();\n\nserver.addService(proto.Calculator.service, {\n    Add: (call: any, cb: any) => {\n        const { a, b } = call.request;\n        cb(null, { result: a + b });\n    },\n});\n\nserver.bindAsync(\"0.0.0.0:50051\", grpc.ServerCredentials.createInsecure(), () => {\n    console.log(\"gRPC server running on 0.0.0.0:50051\");\n});\n",[22,74092,74093,74107,74121,74125,74147,74171,74175,74193,74197,74208,74236,74255,74272,74276,74280,74284,74309,74322],{"__ignoreMap":133},[137,74094,74095,74097,74100,74102,74105],{"class":139,"line":140},[137,74096,10287],{"class":143},[137,74098,74099],{"class":157}," grpc ",[137,74101,10954],{"class":143},[137,74103,74104],{"class":284}," \"@grpc\u002Fgrpc-js\"",[137,74106,3276],{"class":157},[137,74108,74109,74111,74114,74116,74119],{"class":139,"line":173},[137,74110,10287],{"class":143},[137,74112,74113],{"class":157}," protoLoader ",[137,74115,10954],{"class":143},[137,74117,74118],{"class":284}," \"@grpc\u002Fproto-loader\"",[137,74120,3276],{"class":157},[137,74122,74123],{"class":139,"line":188},[137,74124,516],{"emptyLinePlaceholder":515},[137,74126,74127,74129,74132,74134,74137,74140,74142,74145],{"class":139,"line":269},[137,74128,3077],{"class":143},[137,74130,74131],{"class":364}," def",[137,74133,151],{"class":143},[137,74135,74136],{"class":157}," protoLoader.",[137,74138,74139],{"class":147},"loadSync",[137,74141,356],{"class":157},[137,74143,74144],{"class":284},"\"calculator.proto\"",[137,74146,1502],{"class":157},[137,74148,74149,74151,74154,74156,74159,74162,74165,74167,74169],{"class":139,"line":278},[137,74150,3077],{"class":143},[137,74152,74153],{"class":364}," proto",[137,74155,151],{"class":143},[137,74157,74158],{"class":157}," grpc.",[137,74160,74161],{"class":147},"loadPackageDefinition",[137,74163,74164],{"class":157},"(def) ",[137,74166,24431],{"class":143},[137,74168,26137],{"class":364},[137,74170,3276],{"class":157},[137,74172,74173],{"class":139,"line":291},[137,74174,516],{"emptyLinePlaceholder":515},[137,74176,74177,74179,74182,74184,74186,74188,74191],{"class":139,"line":297},[137,74178,3077],{"class":143},[137,74180,74181],{"class":364}," server",[137,74183,151],{"class":143},[137,74185,1426],{"class":143},[137,74187,74158],{"class":157},[137,74189,74190],{"class":147},"Server",[137,74192,924],{"class":157},[137,74194,74195],{"class":139,"line":302},[137,74196,516],{"emptyLinePlaceholder":515},[137,74198,74199,74202,74205],{"class":139,"line":662},[137,74200,74201],{"class":157},"server.",[137,74203,74204],{"class":147},"addService",[137,74206,74207],{"class":157},"(proto.Calculator.service, {\n",[137,74209,74210,74213,74215,74217,74219,74221,74223,74226,74228,74230,74232,74234],{"class":139,"line":667},[137,74211,74212],{"class":147},"    Add",[137,74214,51456],{"class":157},[137,74216,1177],{"class":161},[137,74218,894],{"class":143},[137,74220,26137],{"class":364},[137,74222,164],{"class":157},[137,74224,74225],{"class":161},"cb",[137,74227,894],{"class":143},[137,74229,26137],{"class":364},[137,74231,219],{"class":157},[137,74233,222],{"class":143},[137,74235,256],{"class":157},[137,74237,74238,74240,74242,74244,74246,74248,74250,74252],{"class":139,"line":786},[137,74239,3008],{"class":143},[137,74241,8906],{"class":157},[137,74243,45],{"class":364},[137,74245,164],{"class":157},[137,74247,167],{"class":364},[137,74249,8911],{"class":157},[137,74251,253],{"class":143},[137,74253,74254],{"class":157}," call.request;\n",[137,74256,74257,74260,74262,74264,74267,74269],{"class":139,"line":798},[137,74258,74259],{"class":147},"        cb",[137,74261,356],{"class":157},[137,74263,11700],{"class":364},[137,74265,74266],{"class":157},", { result: a ",[137,74268,182],{"class":143},[137,74270,74271],{"class":157}," b });\n",[137,74273,74274],{"class":139,"line":803},[137,74275,775],{"class":157},[137,74277,74278],{"class":139,"line":931},[137,74279,5422],{"class":157},[137,74281,74282],{"class":139,"line":1568},[137,74283,516],{"emptyLinePlaceholder":515},[137,74285,74286,74288,74291,74293,74296,74299,74302,74305,74307],{"class":139,"line":1573},[137,74287,74201],{"class":157},[137,74289,74290],{"class":147},"bindAsync",[137,74292,356],{"class":157},[137,74294,74295],{"class":284},"\"0.0.0.0:50051\"",[137,74297,74298],{"class":157},", grpc.ServerCredentials.",[137,74300,74301],{"class":147},"createInsecure",[137,74303,74304],{"class":157},"(), () ",[137,74306,222],{"class":143},[137,74308,256],{"class":157},[137,74310,74311,74313,74315,74317,74320],{"class":139,"line":1578},[137,74312,493],{"class":157},[137,74314,353],{"class":147},[137,74316,356],{"class":157},[137,74318,74319],{"class":284},"\"gRPC server running on 0.0.0.0:50051\"",[137,74321,1502],{"class":157},[137,74323,74324],{"class":139,"line":1588},[137,74325,5422],{"class":157},[123,74327,74329],{"id":74328},"grpc-client","gRPC Client",[128,74331,74333],{"className":13299,"code":74332,"language":13301,"meta":133,"style":133},"import grpc from \"@grpc\u002Fgrpc-js\";\nimport protoLoader from \"@grpc\u002Fproto-loader\";\n\nconst def = protoLoader.loadSync(\"calculator.proto\");\nconst proto = grpc.loadPackageDefinition(def) as any;\n\nconst client = new proto.Calculator(\"localhost:50051\", grpc.credentials.createInsecure());\n\nclient.Add({ a: 2, b: 3 }, (_: any, res: any) => {\n    console.log(\"Result:\", res.result);\n});\n",[22,74334,74335,74347,74359,74363,74381,74401,74405,74432,74436,74476,74489],{"__ignoreMap":133},[137,74336,74337,74339,74341,74343,74345],{"class":139,"line":140},[137,74338,10287],{"class":143},[137,74340,74099],{"class":157},[137,74342,10954],{"class":143},[137,74344,74104],{"class":284},[137,74346,3276],{"class":157},[137,74348,74349,74351,74353,74355,74357],{"class":139,"line":173},[137,74350,10287],{"class":143},[137,74352,74113],{"class":157},[137,74354,10954],{"class":143},[137,74356,74118],{"class":284},[137,74358,3276],{"class":157},[137,74360,74361],{"class":139,"line":188},[137,74362,516],{"emptyLinePlaceholder":515},[137,74364,74365,74367,74369,74371,74373,74375,74377,74379],{"class":139,"line":269},[137,74366,3077],{"class":143},[137,74368,74131],{"class":364},[137,74370,151],{"class":143},[137,74372,74136],{"class":157},[137,74374,74139],{"class":147},[137,74376,356],{"class":157},[137,74378,74144],{"class":284},[137,74380,1502],{"class":157},[137,74382,74383,74385,74387,74389,74391,74393,74395,74397,74399],{"class":139,"line":278},[137,74384,3077],{"class":143},[137,74386,74153],{"class":364},[137,74388,151],{"class":143},[137,74390,74158],{"class":157},[137,74392,74161],{"class":147},[137,74394,74164],{"class":157},[137,74396,24431],{"class":143},[137,74398,26137],{"class":364},[137,74400,3276],{"class":157},[137,74402,74403],{"class":139,"line":291},[137,74404,516],{"emptyLinePlaceholder":515},[137,74406,74407,74409,74411,74413,74415,74418,74420,74422,74425,74428,74430],{"class":139,"line":297},[137,74408,3077],{"class":143},[137,74410,15264],{"class":364},[137,74412,151],{"class":143},[137,74414,1426],{"class":143},[137,74416,74417],{"class":157}," proto.",[137,74419,74074],{"class":147},[137,74421,356],{"class":157},[137,74423,74424],{"class":284},"\"localhost:50051\"",[137,74426,74427],{"class":157},", grpc.credentials.",[137,74429,74301],{"class":147},[137,74431,14173],{"class":157},[137,74433,74434],{"class":139,"line":302},[137,74435,516],{"emptyLinePlaceholder":515},[137,74437,74438,74441,74444,74446,74448,74450,74452,74455,74458,74460,74462,74464,74466,74468,74470,74472,74474],{"class":139,"line":662},[137,74439,74440],{"class":157},"client.",[137,74442,74443],{"class":147},"Add",[137,74445,72287],{"class":157},[137,74447,10345],{"class":364},[137,74449,72292],{"class":157},[137,74451,60501],{"class":364},[137,74453,74454],{"class":157}," }, (",[137,74456,74457],{"class":161},"_",[137,74459,894],{"class":143},[137,74461,26137],{"class":364},[137,74463,164],{"class":157},[137,74465,9138],{"class":161},[137,74467,894],{"class":143},[137,74469,26137],{"class":364},[137,74471,219],{"class":157},[137,74473,222],{"class":143},[137,74475,256],{"class":157},[137,74477,74478,74480,74482,74484,74486],{"class":139,"line":667},[137,74479,493],{"class":157},[137,74481,353],{"class":147},[137,74483,356],{"class":157},[137,74485,72345],{"class":284},[137,74487,74488],{"class":157},", res.result);\n",[137,74490,74491],{"class":139,"line":786},[137,74492,5422],{"class":157},[123,74494,74496],{"id":74495},"the-good","The Good",[1003,74498,74499,74505,74511,74519,74525],{},[1006,74500,74501,74504],{},[42,74502,74503],{},"Strong contracts",": Both sides know exactly what to expect",[1006,74506,74507,74510],{},[42,74508,74509],{},"Very fast",": Binary protocol over HTTP\u002F2",[1006,74512,74513,74516,74517,22296],{},[42,74514,74515],{},"Language-agnostic",": Generate clients in Python, Go, Java, and more from the same ",[22,74518,73989],{},[1006,74520,74521,74524],{},[42,74522,74523],{},"Streaming support",": Built-in support for bidirectional streaming",[1006,74526,74527,74530],{},[42,74528,74529],{},"Battle-tested",": Used by Google, Netflix, Slack, and many others",[123,74532,74534],{"id":74533},"the-not-so-good","The Not So Good",[1003,74536,74537,74543,74555,74561],{},[1006,74538,74539,74542],{},[42,74540,74541],{},"More setup",": Requires the proto file, code generation, and gRPC libraries",[1006,74544,74545,74548,74549,74554],{},[42,74546,74547],{},"Less browser-friendly",": Browsers don't natively support gRPC (though ",[45,74550,74553],{"href":74551,"target":2716,"rel":74552},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fgrpc-web",[2718,2719],"gRPC-Web"," exists as a workaround)",[1006,74556,74557,74560],{},[42,74558,74559],{},"Harder to debug",": Binary protocol means you can't just inspect it in the network tab",[1006,74562,74563,74566],{},[42,74564,74565],{},"Learning curve",": Protocol Buffers syntax takes time to learn",[27,74568,74569],{},"gRPC shines when you need performance and type safety, especially for service-to-service communication where browsers aren't involved.",[104,74571,74573],{"id":74572},"trpc-modern-typescript-rpc","tRPC (Modern TypeScript RPC)",[27,74575,74576,74578,74579,74584],{},[42,74577,72605],{}," is one of the newer approaches on this list, created by ",[45,74580,74583],{"href":74581,"target":2716,"rel":74582},"https:\u002F\u002Fx.com\u002Falexdotjs",[2718,2719],"Alex \"KATT\" Johansson"," in 2021. It's built around a simple idea: if both your server and client are written in TypeScript, you can share types directly - eliminating the need for separate API schemas, code generation, or duplicated definitions.",[123,74586,74588],{"id":74587},"the-trpc-philosophy","The tRPC Philosophy",[27,74590,74591],{},"tRPC keeps the RPC model but removes the boilerplate. No schemas. No generated clients. Just TypeScript.",[27,74593,74594],{},"You define your procedures on the server, export the type, and import it on the client. TypeScript's type inference handles the rest.",[123,74596,74598],{"id":74597},"trpc-server","tRPC Server",[128,74600,74602],{"className":13299,"code":74601,"language":13301,"meta":133,"style":133},"import { initTRPC } from \"@trpc\u002Fserver\";\nimport { createHTTPServer } from \"@trpc\u002Fserver\u002Fadapters\u002Fstandalone\";\n\nconst t = initTRPC.create();\n\nconst appRouter = t.router({\n    add: t.procedure\n        .input((v: unknown) => v as [number, number])\n        .query(({ input }) => {\n            const [a, b] = input;\n            return a + b;\n        }),\n});\n\nexport type AppRouter = typeof appRouter;\n\ncreateHTTPServer({ router: appRouter }).listen(3000);\n\nconsole.log(\"tRPC server running on http:\u002F\u002Flocalhost:3000\");\n",[22,74603,74604,74618,74632,74636,74652,74656,74673,74678,74716,74733,74752,74762,74766,74770,74774,74790,74794,74810,74814],{"__ignoreMap":133},[137,74605,74606,74608,74611,74613,74616],{"class":139,"line":140},[137,74607,10287],{"class":143},[137,74609,74610],{"class":157}," { initTRPC } ",[137,74612,10954],{"class":143},[137,74614,74615],{"class":284}," \"@trpc\u002Fserver\"",[137,74617,3276],{"class":157},[137,74619,74620,74622,74625,74627,74630],{"class":139,"line":173},[137,74621,10287],{"class":143},[137,74623,74624],{"class":157}," { createHTTPServer } ",[137,74626,10954],{"class":143},[137,74628,74629],{"class":284}," \"@trpc\u002Fserver\u002Fadapters\u002Fstandalone\"",[137,74631,3276],{"class":157},[137,74633,74634],{"class":139,"line":188},[137,74635,516],{"emptyLinePlaceholder":515},[137,74637,74638,74640,74643,74645,74648,74650],{"class":139,"line":269},[137,74639,3077],{"class":143},[137,74641,74642],{"class":364}," t",[137,74644,151],{"class":143},[137,74646,74647],{"class":157}," initTRPC.",[137,74649,15075],{"class":147},[137,74651,924],{"class":157},[137,74653,74654],{"class":139,"line":278},[137,74655,516],{"emptyLinePlaceholder":515},[137,74657,74658,74660,74663,74665,74668,74671],{"class":139,"line":291},[137,74659,3077],{"class":143},[137,74661,74662],{"class":364}," appRouter",[137,74664,151],{"class":143},[137,74666,74667],{"class":157}," t.",[137,74669,74670],{"class":147},"router",[137,74672,3175],{"class":157},[137,74674,74675],{"class":139,"line":297},[137,74676,74677],{"class":157},"    add: t.procedure\n",[137,74679,74680,74683,74685,74687,74690,74692,74695,74697,74699,74702,74704,74706,74709,74711,74713],{"class":139,"line":302},[137,74681,74682],{"class":157},"        .",[137,74684,8520],{"class":147},[137,74686,2774],{"class":157},[137,74688,74689],{"class":161},"v",[137,74691,894],{"class":143},[137,74693,74694],{"class":364}," unknown",[137,74696,219],{"class":157},[137,74698,222],{"class":143},[137,74700,74701],{"class":157}," v ",[137,74703,24431],{"class":143},[137,74705,22130],{"class":157},[137,74707,74708],{"class":364},"number",[137,74710,164],{"class":157},[137,74712,74708],{"class":364},[137,74714,74715],{"class":157},"])\n",[137,74717,74718,74720,74722,74725,74727,74729,74731],{"class":139,"line":662},[137,74719,74682],{"class":157},[137,74721,49767],{"class":147},[137,74723,74724],{"class":157},"(({ ",[137,74726,8520],{"class":161},[137,74728,29299],{"class":157},[137,74730,222],{"class":143},[137,74732,256],{"class":157},[137,74734,74735,74737,74739,74741,74743,74745,74747,74749],{"class":139,"line":667},[137,74736,5772],{"class":143},[137,74738,22130],{"class":157},[137,74740,45],{"class":364},[137,74742,164],{"class":157},[137,74744,167],{"class":364},[137,74746,5796],{"class":157},[137,74748,253],{"class":143},[137,74750,74751],{"class":157}," input;\n",[137,74753,74754,74756,74758,74760],{"class":139,"line":786},[137,74755,4683],{"class":143},[137,74757,179],{"class":157},[137,74759,182],{"class":143},[137,74761,185],{"class":157},[137,74763,74764],{"class":139,"line":798},[137,74765,13426],{"class":157},[137,74767,74768],{"class":139,"line":803},[137,74769,5422],{"class":157},[137,74771,74772],{"class":139,"line":931},[137,74773,516],{"emptyLinePlaceholder":515},[137,74775,74776,74778,74780,74783,74785,74787],{"class":139,"line":1568},[137,74777,13456],{"class":143},[137,74779,25639],{"class":143},[137,74781,74782],{"class":147}," AppRouter",[137,74784,151],{"class":143},[137,74786,35945],{"class":143},[137,74788,74789],{"class":157}," appRouter;\n",[137,74791,74792],{"class":139,"line":1573},[137,74793,516],{"emptyLinePlaceholder":515},[137,74795,74796,74799,74802,74804,74806,74808],{"class":139,"line":1578},[137,74797,74798],{"class":147},"createHTTPServer",[137,74800,74801],{"class":157},"({ router: appRouter }).",[137,74803,15106],{"class":147},[137,74805,356],{"class":157},[137,74807,15111],{"class":364},[137,74809,1502],{"class":157},[137,74811,74812],{"class":139,"line":1588},[137,74813,516],{"emptyLinePlaceholder":515},[137,74815,74816,74818,74820,74822,74825],{"class":139,"line":1601},[137,74817,1436],{"class":157},[137,74819,353],{"class":147},[137,74821,356],{"class":157},[137,74823,74824],{"class":284},"\"tRPC server running on http:\u002F\u002Flocalhost:3000\"",[137,74826,1502],{"class":157},[123,74828,74830],{"id":74829},"trpc-client","tRPC Client",[128,74832,74834],{"className":13299,"code":74833,"language":13301,"meta":133,"style":133},"import { createTRPCProxyClient, httpBatchLink } from \"@trpc\u002Fclient\";\nimport type { AppRouter } from \".\u002Fserver.js\";\n\nconst client = createTRPCProxyClient\u003CAppRouter>({\n    links: [\n        httpBatchLink({\n            url: \"http:\u002F\u002Flocalhost:3000\",\n        }),\n    ],\n});\n\n(async () => {\n    const result = await client.add.query([2, 3]);\n    console.log(\"Result:\", result);\n})();\n",[22,74835,74836,74850,74866,74870,74889,74894,74901,74910,74914,74918,74922,74926,74938,74963,74976],{"__ignoreMap":133},[137,74837,74838,74840,74843,74845,74848],{"class":139,"line":140},[137,74839,10287],{"class":143},[137,74841,74842],{"class":157}," { createTRPCProxyClient, httpBatchLink } ",[137,74844,10954],{"class":143},[137,74846,74847],{"class":284}," \"@trpc\u002Fclient\"",[137,74849,3276],{"class":157},[137,74851,74852,74854,74856,74859,74861,74864],{"class":139,"line":173},[137,74853,10287],{"class":143},[137,74855,25639],{"class":143},[137,74857,74858],{"class":157}," { AppRouter } ",[137,74860,10954],{"class":143},[137,74862,74863],{"class":284}," \".\u002Fserver.js\"",[137,74865,3276],{"class":157},[137,74867,74868],{"class":139,"line":188},[137,74869,516],{"emptyLinePlaceholder":515},[137,74871,74872,74874,74876,74878,74881,74883,74886],{"class":139,"line":269},[137,74873,3077],{"class":143},[137,74875,15264],{"class":364},[137,74877,151],{"class":143},[137,74879,74880],{"class":147}," createTRPCProxyClient",[137,74882,4033],{"class":157},[137,74884,74885],{"class":147},"AppRouter",[137,74887,74888],{"class":157},">({\n",[137,74890,74891],{"class":139,"line":278},[137,74892,74893],{"class":157},"    links: [\n",[137,74895,74896,74899],{"class":139,"line":291},[137,74897,74898],{"class":147},"        httpBatchLink",[137,74900,3175],{"class":157},[137,74902,74903,74906,74908],{"class":139,"line":297},[137,74904,74905],{"class":157},"            url: ",[137,74907,73118],{"class":284},[137,74909,1961],{"class":157},[137,74911,74912],{"class":139,"line":302},[137,74913,13426],{"class":157},[137,74915,74916],{"class":139,"line":662},[137,74917,13436],{"class":157},[137,74919,74920],{"class":139,"line":667},[137,74921,5422],{"class":157},[137,74923,74924],{"class":139,"line":786},[137,74925,516],{"emptyLinePlaceholder":515},[137,74927,74928,74930,74932,74934,74936],{"class":139,"line":798},[137,74929,356],{"class":157},[137,74931,15050],{"class":143},[137,74933,1484],{"class":157},[137,74935,222],{"class":143},[137,74937,256],{"class":157},[137,74939,74940,74942,74944,74946,74948,74951,74953,74955,74957,74959,74961],{"class":139,"line":803},[137,74941,4177],{"class":143},[137,74943,26939],{"class":364},[137,74945,151],{"class":143},[137,74947,15069],{"class":143},[137,74949,74950],{"class":157}," client.add.",[137,74952,49767],{"class":147},[137,74954,43550],{"class":157},[137,74956,10345],{"class":364},[137,74958,164],{"class":157},[137,74960,60501],{"class":364},[137,74962,43556],{"class":157},[137,74964,74965,74967,74969,74971,74973],{"class":139,"line":931},[137,74966,493],{"class":157},[137,74968,353],{"class":147},[137,74970,356],{"class":157},[137,74972,72345],{"class":284},[137,74974,74975],{"class":157},", result);\n",[137,74977,74978],{"class":139,"line":1568},[137,74979,74980],{"class":157},"})();\n",[123,74982,74984],{"id":74983},"the-magic","The Magic",[27,74986,74987,74988,74991,74992,74994],{},"See that ",[22,74989,74990],{},"import type { AppRouter }","? That's the secret. The client imports only the ",[42,74993,20355],{}," from the server - not the actual code. This means:",[1003,74996,74997,75000,75003,75006],{},[1006,74998,74999],{},"Your IDE knows exactly what methods are available",[1006,75001,75002],{},"You get autocomplete for method names and parameters",[1006,75004,75005],{},"Type errors appear before you even run the code",[1006,75007,75008],{},"Refactor a procedure name, and TypeScript shows you everywhere it's used",[123,75010,74496],{"id":75011},"the-good-1",[1003,75013,75014,75020,75026,75035,75041],{},[1006,75015,75016,75019],{},[42,75017,75018],{},"End-to-end type safety",": Change the server, and TypeScript tells you what broke on the client",[1006,75021,75022,75025],{},[42,75023,75024],{},"No API duplication",": Define once, use everywhere",[1006,75027,75028,726,75031,75034],{},[42,75029,75030],{},"Feels like local functions",[22,75032,75033],{},"client.add.query([2, 3])"," feels natural",[1006,75036,75037,75040],{},[42,75038,75039],{},"Great developer experience",": Autocomplete, type checking, and refactoring support",[1006,75042,75043,75046],{},[42,75044,75045],{},"Growing ecosystem",": Integrates well with React, Next.js, and other frameworks",[123,75048,74534],{"id":75049},"the-not-so-good-1",[1003,75051,75052,75058,75064],{},[1006,75053,75054,75057],{},[42,75055,75056],{},"TypeScript-only",": Both server and client must be TypeScript",[1006,75059,75060,75063],{},[42,75061,75062],{},"Same codebase (sort of)",": Type sharing works best in monorepos or when you can share types",[1006,75065,75066,75069],{},[42,75067,75068],{},"Newer",": Less battle-tested than gRPC, smaller community",[27,75071,75072],{},"tRPC is perfect for full-stack TypeScript applications where you control both ends.",[104,75074,75076],{"id":75075},"orpc-type-safety-meets-openapi","oRPC (Type Safety Meets OpenAPI)",[27,75078,75079],{},"oRPC is a newer TypeScript RPC framework that combines end-to-end type safety with native OpenAPI support. It's designed for developers who want type-safe APIs that also generate standard documentation.",[123,75081,75083],{"id":75082},"the-orpc-philosophy","The oRPC Philosophy",[27,75085,75086],{},"Like tRPC, you define procedures on the server and import types on the client. Unlike tRPC, oRPC uses explicit schema validation (with Zod or other validators), which enables automatic OpenAPI generation.",[123,75088,75090],{"id":75089},"orpc-server","oRPC Server",[128,75092,75094],{"className":13299,"code":75093,"language":13301,"meta":133,"style":133},"import { createServer } from \"node:http\";\nimport { os } from \"@orpc\u002Fserver\";\nimport { RPCHandler } from \"@orpc\u002Fserver\u002Fnode\";\nimport { z } from \"zod\";\n\nconst add = os.input(z.object({ a: z.number(), b: z.number() })).handler(async ({ input }) => {\n    return input.a + input.b;\n});\n\nexport const router = {\n    add,\n};\n\nexport type Router = typeof router;\n\nconst handler = new RPCHandler(router);\n\nconst server = createServer(async (req, res) => {\n    const result = await handler.handle(req, res, {\n        context: {},\n    });\n\n    if (!result.matched) {\n        res.statusCode = 404;\n        res.end(\"No procedure matched\");\n    }\n});\n\nserver.listen(3000, \"127.0.0.1\", () => {\n    console.log(\"oRPC server running on http:\u002F\u002F127.0.0.1:3000\");\n});\n",[22,75095,75096,75110,75124,75138,75152,75156,75205,75217,75221,75225,75238,75243,75247,75251,75267,75271,75287,75291,75320,75337,75342,75346,75350,75361,75372,75385,75389,75393,75397,75418,75431],{"__ignoreMap":133},[137,75097,75098,75100,75103,75105,75108],{"class":139,"line":140},[137,75099,10287],{"class":143},[137,75101,75102],{"class":157}," { createServer } ",[137,75104,10954],{"class":143},[137,75106,75107],{"class":284}," \"node:http\"",[137,75109,3276],{"class":157},[137,75111,75112,75114,75117,75119,75122],{"class":139,"line":173},[137,75113,10287],{"class":143},[137,75115,75116],{"class":157}," { os } ",[137,75118,10954],{"class":143},[137,75120,75121],{"class":284}," \"@orpc\u002Fserver\"",[137,75123,3276],{"class":157},[137,75125,75126,75128,75131,75133,75136],{"class":139,"line":188},[137,75127,10287],{"class":143},[137,75129,75130],{"class":157}," { RPCHandler } ",[137,75132,10954],{"class":143},[137,75134,75135],{"class":284}," \"@orpc\u002Fserver\u002Fnode\"",[137,75137,3276],{"class":157},[137,75139,75140,75142,75145,75147,75150],{"class":139,"line":269},[137,75141,10287],{"class":143},[137,75143,75144],{"class":157}," { z } ",[137,75146,10954],{"class":143},[137,75148,75149],{"class":284}," \"zod\"",[137,75151,3276],{"class":157},[137,75153,75154],{"class":139,"line":278},[137,75155,516],{"emptyLinePlaceholder":515},[137,75157,75158,75160,75162,75164,75167,75169,75172,75175,75178,75180,75183,75185,75188,75190,75192,75194,75197,75199,75201,75203],{"class":139,"line":291},[137,75159,3077],{"class":143},[137,75161,17266],{"class":364},[137,75163,151],{"class":143},[137,75165,75166],{"class":157}," os.",[137,75168,8520],{"class":147},[137,75170,75171],{"class":157},"(z.",[137,75173,75174],{"class":147},"object",[137,75176,75177],{"class":157},"({ a: z.",[137,75179,74708],{"class":147},[137,75181,75182],{"class":157},"(), b: z.",[137,75184,74708],{"class":147},[137,75186,75187],{"class":157},"() })).",[137,75189,20132],{"class":147},[137,75191,356],{"class":157},[137,75193,15050],{"class":143},[137,75195,75196],{"class":157}," ({ ",[137,75198,8520],{"class":161},[137,75200,29299],{"class":157},[137,75202,222],{"class":143},[137,75204,256],{"class":157},[137,75206,75207,75209,75212,75214],{"class":139,"line":297},[137,75208,176],{"class":143},[137,75210,75211],{"class":157}," input.a ",[137,75213,182],{"class":143},[137,75215,75216],{"class":157}," input.b;\n",[137,75218,75219],{"class":139,"line":302},[137,75220,5422],{"class":157},[137,75222,75223],{"class":139,"line":662},[137,75224,516],{"emptyLinePlaceholder":515},[137,75226,75227,75229,75231,75234,75236],{"class":139,"line":667},[137,75228,13456],{"class":143},[137,75230,20388],{"class":143},[137,75232,75233],{"class":364}," router",[137,75235,151],{"class":143},[137,75237,256],{"class":157},[137,75239,75240],{"class":139,"line":786},[137,75241,75242],{"class":157},"    add,\n",[137,75244,75245],{"class":139,"line":798},[137,75246,191],{"class":157},[137,75248,75249],{"class":139,"line":803},[137,75250,516],{"emptyLinePlaceholder":515},[137,75252,75253,75255,75257,75260,75262,75264],{"class":139,"line":931},[137,75254,13456],{"class":143},[137,75256,25639],{"class":143},[137,75258,75259],{"class":147}," Router",[137,75261,151],{"class":143},[137,75263,35945],{"class":143},[137,75265,75266],{"class":157}," router;\n",[137,75268,75269],{"class":139,"line":1568},[137,75270,516],{"emptyLinePlaceholder":515},[137,75272,75273,75275,75277,75279,75281,75284],{"class":139,"line":1573},[137,75274,3077],{"class":143},[137,75276,33135],{"class":364},[137,75278,151],{"class":143},[137,75280,1426],{"class":143},[137,75282,75283],{"class":147}," RPCHandler",[137,75285,75286],{"class":157},"(router);\n",[137,75288,75289],{"class":139,"line":1578},[137,75290,516],{"emptyLinePlaceholder":515},[137,75292,75293,75295,75297,75299,75302,75304,75306,75308,75310,75312,75314,75316,75318],{"class":139,"line":1588},[137,75294,3077],{"class":143},[137,75296,74181],{"class":364},[137,75298,151],{"class":143},[137,75300,75301],{"class":147}," createServer",[137,75303,356],{"class":157},[137,75305,15050],{"class":143},[137,75307,158],{"class":157},[137,75309,9133],{"class":161},[137,75311,164],{"class":157},[137,75313,9138],{"class":161},[137,75315,219],{"class":157},[137,75317,222],{"class":143},[137,75319,256],{"class":157},[137,75321,75322,75324,75326,75328,75330,75332,75334],{"class":139,"line":1601},[137,75323,4177],{"class":143},[137,75325,26939],{"class":364},[137,75327,151],{"class":143},[137,75329,15069],{"class":143},[137,75331,20255],{"class":157},[137,75333,20258],{"class":147},[137,75335,75336],{"class":157},"(req, res, {\n",[137,75338,75339],{"class":139,"line":3802},[137,75340,75341],{"class":157},"        context: {},\n",[137,75343,75344],{"class":139,"line":3808},[137,75345,2832],{"class":157},[137,75347,75348],{"class":139,"line":3822},[137,75349,516],{"emptyLinePlaceholder":515},[137,75351,75352,75354,75356,75358],{"class":139,"line":3827},[137,75353,24696],{"class":143},[137,75355,158],{"class":157},[137,75357,17393],{"class":143},[137,75359,75360],{"class":157},"result.matched) {\n",[137,75362,75363,75365,75367,75370],{"class":139,"line":3832},[137,75364,33002],{"class":157},[137,75366,253],{"class":143},[137,75368,75369],{"class":364}," 404",[137,75371,3276],{"class":157},[137,75373,75374,75376,75378,75380,75383],{"class":139,"line":3840},[137,75375,33014],{"class":157},[137,75377,33017],{"class":147},[137,75379,356],{"class":157},[137,75381,75382],{"class":284},"\"No procedure matched\"",[137,75384,1502],{"class":157},[137,75386,75387],{"class":139,"line":3846},[137,75388,294],{"class":157},[137,75390,75391],{"class":139,"line":3861},[137,75392,5422],{"class":157},[137,75394,75395],{"class":139,"line":3883},[137,75396,516],{"emptyLinePlaceholder":515},[137,75398,75399,75401,75403,75405,75407,75409,75412,75414,75416],{"class":139,"line":3896},[137,75400,74201],{"class":157},[137,75402,15106],{"class":147},[137,75404,356],{"class":157},[137,75406,15111],{"class":364},[137,75408,164],{"class":157},[137,75410,75411],{"class":284},"\"127.0.0.1\"",[137,75413,4420],{"class":157},[137,75415,222],{"class":143},[137,75417,256],{"class":157},[137,75419,75420,75422,75424,75426,75429],{"class":139,"line":3901},[137,75421,493],{"class":157},[137,75423,353],{"class":147},[137,75425,356],{"class":157},[137,75427,75428],{"class":284},"\"oRPC server running on http:\u002F\u002F127.0.0.1:3000\"",[137,75430,1502],{"class":157},[137,75432,75433],{"class":139,"line":3906},[137,75434,5422],{"class":157},[123,75436,75438],{"id":75437},"orpc-client","oRPC Client",[128,75440,75442],{"className":13299,"code":75441,"language":13301,"meta":133,"style":133},"import { createORPCClient } from \"@orpc\u002Fclient\";\nimport { RPCLink } from \"@orpc\u002Fclient\u002Ffetch\";\nimport type { Router } from \".\u002Fserver.js\";\n\nconst link = new RPCLink({\n    url: \"http:\u002F\u002F127.0.0.1:3000\",\n});\n\nconst client = createORPCClient\u003CRouter>(link);\n\n(async () => {\n    const result = await client.add({ a: 2, b: 3 });\n    console.log(\"Result:\", result);\n})();\n",[22,75443,75444,75458,75472,75487,75491,75506,75516,75520,75524,75543,75547,75559,75584,75596],{"__ignoreMap":133},[137,75445,75446,75448,75451,75453,75456],{"class":139,"line":140},[137,75447,10287],{"class":143},[137,75449,75450],{"class":157}," { createORPCClient } ",[137,75452,10954],{"class":143},[137,75454,75455],{"class":284}," \"@orpc\u002Fclient\"",[137,75457,3276],{"class":157},[137,75459,75460,75462,75465,75467,75470],{"class":139,"line":173},[137,75461,10287],{"class":143},[137,75463,75464],{"class":157}," { RPCLink } ",[137,75466,10954],{"class":143},[137,75468,75469],{"class":284}," \"@orpc\u002Fclient\u002Ffetch\"",[137,75471,3276],{"class":157},[137,75473,75474,75476,75478,75481,75483,75485],{"class":139,"line":188},[137,75475,10287],{"class":143},[137,75477,25639],{"class":143},[137,75479,75480],{"class":157}," { Router } ",[137,75482,10954],{"class":143},[137,75484,74863],{"class":284},[137,75486,3276],{"class":157},[137,75488,75489],{"class":139,"line":269},[137,75490,516],{"emptyLinePlaceholder":515},[137,75492,75493,75495,75497,75499,75501,75504],{"class":139,"line":278},[137,75494,3077],{"class":143},[137,75496,34282],{"class":364},[137,75498,151],{"class":143},[137,75500,1426],{"class":143},[137,75502,75503],{"class":147}," RPCLink",[137,75505,3175],{"class":157},[137,75507,75508,75511,75514],{"class":139,"line":291},[137,75509,75510],{"class":157},"    url: ",[137,75512,75513],{"class":284},"\"http:\u002F\u002F127.0.0.1:3000\"",[137,75515,1961],{"class":157},[137,75517,75518],{"class":139,"line":297},[137,75519,5422],{"class":157},[137,75521,75522],{"class":139,"line":302},[137,75523,516],{"emptyLinePlaceholder":515},[137,75525,75526,75528,75530,75532,75535,75537,75540],{"class":139,"line":662},[137,75527,3077],{"class":143},[137,75529,15264],{"class":364},[137,75531,151],{"class":143},[137,75533,75534],{"class":147}," createORPCClient",[137,75536,4033],{"class":157},[137,75538,75539],{"class":147},"Router",[137,75541,75542],{"class":157},">(link);\n",[137,75544,75545],{"class":139,"line":667},[137,75546,516],{"emptyLinePlaceholder":515},[137,75548,75549,75551,75553,75555,75557],{"class":139,"line":786},[137,75550,356],{"class":157},[137,75552,15050],{"class":143},[137,75554,1484],{"class":157},[137,75556,222],{"class":143},[137,75558,256],{"class":157},[137,75560,75561,75563,75565,75567,75569,75572,75574,75576,75578,75580,75582],{"class":139,"line":798},[137,75562,4177],{"class":143},[137,75564,26939],{"class":364},[137,75566,151],{"class":143},[137,75568,15069],{"class":143},[137,75570,75571],{"class":157}," client.",[137,75573,34393],{"class":147},[137,75575,72287],{"class":157},[137,75577,10345],{"class":364},[137,75579,72292],{"class":157},[137,75581,60501],{"class":364},[137,75583,4168],{"class":157},[137,75585,75586,75588,75590,75592,75594],{"class":139,"line":803},[137,75587,493],{"class":157},[137,75589,353],{"class":147},[137,75591,356],{"class":157},[137,75593,72345],{"class":284},[137,75595,74975],{"class":157},[137,75597,75598],{"class":139,"line":931},[137,75599,74980],{"class":157},[123,75601,74496],{"id":75602},"the-good-2",[1003,75604,75605,75610,75616,75622,75628],{},[1006,75606,75607,75609],{},[42,75608,75018],{},": Like tRPC, change the server and TypeScript shows what broke",[1006,75611,75612,75615],{},[42,75613,75614],{},"Native OpenAPI support",": Automatically generates API documentation from your schemas",[1006,75617,75618,75621],{},[42,75619,75620],{},"Built-in schema validation",": Zod validates inputs at runtime, catching errors early",[1006,75623,75624,75627],{},[42,75625,75626],{},"Multi-runtime",": Works on Node.js, Deno, Bun, and Cloudflare Workers",[1006,75629,75630,75633],{},[42,75631,75632],{},"Explicit contracts",": Zod schemas serve as both documentation and validation",[123,75635,74534],{"id":75636},"the-not-so-good-2",[1003,75638,75639,75645],{},[1006,75640,75641,75644],{},[42,75642,75643],{},"Requires schema definitions",": More verbose than tRPC's implicit typing",[1006,75646,75647,75650],{},[42,75648,75649],{},"Newer framework",": Smaller community, less battle-tested",[104,75652,75654],{"id":75653},"when-should-you-use-rpc-vs-rest",[42,75655,75656],{},"When Should You Use RPC vs. REST?",[27,75658,75659],{},"By now, it should be clear that RPC and REST aren't competitors - they solve different problems and excel in different contexts. The right choice depends on your team structure, tooling, and the system you're building.",[123,75661,75663],{"id":75662},"use-rest-when",[42,75664,75665],{},"Use REST When:",[27,75667,75668],{},"REST is an excellent choice when:",[1003,75670,75671,75677,75700,75709,75715],{},[1006,75672,75673,75676],{},[42,75674,75675],{},"You're building public or third-party APIs"," - REST is universally understood. Almost every language, tool, and platform speaks HTTP + JSON.",[1006,75678,75679,75682,75683],{},[42,75680,75681],{},"Your API is resource-oriented"," - CRUD-heavy systems map naturally to REST:\n",[1003,75684,75685,75690,75695],{},[1006,75686,75687],{},[22,75688,75689],{},"\u002Fusers",[1006,75691,75692],{},[22,75693,75694],{},"\u002Forders",[1006,75696,75697],{},[22,75698,75699],{},"\u002Fproducts\u002F{id}",[1006,75701,75702,75705,75706,75708],{},[42,75703,75704],{},"You need strong browser compatibility"," - REST works out of the box with ",[22,75707,59737],{},", dev tools, CDNs, caching layers, and proxies.",[1006,75710,75711,75714],{},[42,75712,75713],{},"Your client and server evolve independently"," - REST's looser coupling makes it easier to version APIs and support older clients.",[1006,75716,75717,75720],{},[42,75718,75719],{},"You want simple caching and observability"," - HTTP semantics (status codes, caching headers, proxies) work naturally with REST.",[27,75722,75723],{},"REST excels when stability, interoperability, and simplicity matter more than tight coupling or type precision.",[123,75725,75727],{"id":75726},"use-rpc-when",[42,75728,75729],{},"Use RPC When:",[27,75731,75732],{},"RPC becomes a better fit when:",[1003,75734,75735,75741,75757,75763,75769],{},[1006,75736,75737,75740],{},[42,75738,75739],{},"You control both the client and the server"," - This is especially true in monorepos or tightly coordinated teams.",[1006,75742,75743,75746,75747,164,75750,29088,75753,75756],{},[42,75744,75745],{},"Your API is action-oriented, not resource-oriented"," - Operations like ",[22,75748,75749],{},"calculateInvoice",[22,75751,75752],{},"recommendMovies",[22,75754,75755],{},"validateCheckout"," feel much more natural as function calls than as REST endpoints.",[1006,75758,75759,75762],{},[42,75760,75761],{},"End-to-end type safety matters"," - If runtime errors from API mismatches are costly, RPC frameworks like tRPC, oRPC, or gRPC dramatically reduce that risk.",[1006,75764,75765,75768],{},[42,75766,75767],{},"Developer experience is a priority"," - Autocomplete, refactoring confidence, and compile-time feedback improve productivity.",[1006,75770,75771,75774],{},[42,75772,75773],{},"You're building internal services or microservices"," - gRPC shines for service-to-service communication where performance and contracts matter more than browser compatibility.",[27,75776,75777],{},"RPC works best when you want your backend to feel like a shared library, not a distant HTTP interface.",[104,75779,75780],{"id":2566},[42,75781,2567],{},[27,75783,75784],{},"RPC isn't new - but it feels new again because modern tooling finally delivers on its original promise.",[27,75786,75787],{},"REST gave us a universal language for the web, and it's not going anywhere. It remains an excellent choice for resource-based APIs, public interfaces, and loosely coupled systems.",[27,75789,75790],{},"RPC flips the mental model. Instead of thinking in endpoints and resources, you think in functions and contracts. Modern frameworks like gRPC, tRPC, and oRPC bring type safety, performance, and developer experience to the forefront - making remote calls feel almost indistinguishable from local ones.",[27,75792,75793,75794],{},"The real takeaway isn't \"RPC is better than REST.\" It's this: ",[42,75795,75796],{},"choose the model that matches how your system actually behaves.",[1003,75798,75799,75802],{},[1006,75800,75801],{},"If your API feels like a set of actions - use RPC.",[1006,75803,75804],{},"If it feels like a set of resources - REST is a great fit.",[27,75806,75807],{},"And if you're lucky enough to use both, you'll get the best of both worlds 🙂.",[27,75809,75810,75811,1017],{},"All the examples above have been included in a GitHub repo ",[45,75812,10647],{"href":75813,"target":2716,"rel":75814},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fbasic-examples-of-rpc",[2718,2719],[2617,75816,75817],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":133,"searchDepth":173,"depth":173,"links":75819},[75820,75821,75826,75827,75828,75829,75830,75831,75832,75839,75848,75849,75857,75865,75872,75876],{"id":71992,"depth":173,"text":71993},{"id":72010,"depth":173,"text":72011,"children":75822},[75823,75824,75825],{"id":72022,"depth":188,"text":72023},{"id":72248,"depth":188,"text":15937},{"id":72394,"depth":188,"text":72395},{"id":72433,"depth":173,"text":72434},{"id":72500,"depth":173,"text":72501},{"id":72520,"depth":173,"text":72523},{"id":72623,"depth":173,"text":72624},{"id":72645,"depth":173,"text":72646},{"id":72747,"depth":173,"text":72748},{"id":72765,"depth":173,"text":72766,"children":75833},[75834,75835,75836,75837,75838],{"id":36647,"depth":188,"text":72772},{"id":72795,"depth":188,"text":72796},{"id":73078,"depth":188,"text":73079},{"id":73227,"depth":188,"text":73228},{"id":73242,"depth":188,"text":73243},{"id":73266,"depth":173,"text":73267,"children":75840},[75841,75842,75843,75844,75845,75846,75847],{"id":73276,"depth":188,"text":73277},{"id":73300,"depth":188,"text":73301},{"id":73371,"depth":188,"text":73372},{"id":73421,"depth":188,"text":73422},{"id":73732,"depth":188,"text":73733},{"id":73866,"depth":188,"text":73228},{"id":73883,"depth":188,"text":73243},{"id":73909,"depth":173,"text":73910},{"id":73943,"depth":173,"text":73944,"children":75850},[75851,75852,75853,75854,75855,75856],{"id":73978,"depth":188,"text":73979},{"id":73996,"depth":188,"text":73997},{"id":74086,"depth":188,"text":74087},{"id":74328,"depth":188,"text":74329},{"id":74495,"depth":188,"text":74496},{"id":74533,"depth":188,"text":74534},{"id":74572,"depth":173,"text":74573,"children":75858},[75859,75860,75861,75862,75863,75864],{"id":74587,"depth":188,"text":74588},{"id":74597,"depth":188,"text":74598},{"id":74829,"depth":188,"text":74830},{"id":74983,"depth":188,"text":74984},{"id":75011,"depth":188,"text":74496},{"id":75049,"depth":188,"text":74534},{"id":75075,"depth":173,"text":75076,"children":75866},[75867,75868,75869,75870,75871],{"id":75082,"depth":188,"text":75083},{"id":75089,"depth":188,"text":75090},{"id":75437,"depth":188,"text":75438},{"id":75602,"depth":188,"text":74496},{"id":75636,"depth":188,"text":74534},{"id":75653,"depth":173,"text":75656,"children":75873},[75874,75875],{"id":75662,"depth":188,"text":75665},{"id":75726,"depth":188,"text":75729},{"id":2566,"depth":173,"text":2567},"Explore modern RPC frameworks and learn how they differ from REST. This comprehensive guide covers plain RPC, JSON-RPC, gRPC, tRPC, and oRPC with practical examples. Discover when to use RPC over REST, understand end-to-end type safety, and see how modern tools like tRPC and oRPC simplify full-stack TypeScript development while gRPC excels in microservices architecture.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1768564822\u002Fblog\u002Funderstanding-modern-rpc-frameworks-how-they-work-and-when-to-use-them\u002Funderstanding-modern-rpc-frameworks-how-they-work-and-when-to-use-them_pkdooe",[75880,75881,75882,72600,72605,72610,72591,75883,75884,75885,75886,75887,75888,75889,75890,72414,75891,75892,75893,75894],"RPC frameworks","Remote Procedure Call","REST vs RPC","Protocol Buffers","end-to-end type safety","TypeScript RPC","microservices communication","API design","HTTP\u002F2","Node.js RPC","full-stack TypeScript","Zod validation","type-safe APIs","client-server communication","modern web architecture",{},"\u002F2026\u002F01\u002F18\u002Funderstanding-modern-rpc-frameworks-how-they-work-and-when-to-use-them","18th January 2026",{"title":71927,"description":75877},"2026\u002F01\u002F18\u002Funderstanding-modern-rpc-frameworks-how-they-work-and-when-to-use-them","_Hp3jTqXkhEv1eXPEu6X8CCYmS1oyvFg-8kB1eI9m9s",{"id":75902,"title":75903,"articleTags":75904,"author":11,"blog":12,"body":75906,"description":80382,"extension":2649,"image":80383,"keywords":80384,"meta":80404,"navigation":515,"path":80405,"published":80406,"readTime":302,"seo":80407,"stem":80408,"type":2662,"__hash__":80409},"content\u002F2026\u002F02\u002F01\u002Fhow-to-run-and-debug-your-github-workflows-locally.md","How to Run and Debug Your GitHub Workflows Locally",[75905,71449,22224],"DevOps",{"type":14,"value":75907,"toc":80360},[75908,75911,75925,75927,75931,75936,75945,75948,75955,75962,75973,75982,75992,75995,75999,76005,76008,76011,76031,76044,76053,76056,76058,76061,76091,76096,76099,76103,76106,76123,76126,76129,76142,76145,76151,76162,76166,76172,76186,76189,76200,76203,76209,76212,76216,76222,76469,76478,76481,76493,76499,76519,76525,76531,76537,76540,76544,76549,76556,76562,76568,76571,76592,76596,76599,76604,76854,76861,76879,76885,76889,76892,76898,77062,77068,77376,77379,77401,77407,77411,77417,77422,77920,77926,78150,78152,78173,78177,78180,78185,78621,78624,78681,78684,78688,78691,78696,79158,79161,79194,79196,79212,79216,79219,79224,79427,79430,79452,79456,79459,79464,79684,79687,79761,79772,79776,79779,79788,79793,79851,79856,79862,79865,79879,79885,80225,80227,80243,80246,80250,80261,80267,80271,80274,80293,80299,80302,80306,80313,80330,80333,80335,80338,80341,80344,80354,80357],[17,75909,75903],{"id":75910},"how-to-run-and-debug-your-github-workflows-locally",[27,75912,75913],{},[30,75914,75915,36,75917,40,75919],{},[33,75916],{"value":35},[33,75918],{"value":39},[42,75920,75921],{},[45,75922,75923],{"href":47},[33,75924],{"value":50},[52,75926],{":tags":54},[56,75928],{":audio-src":75929,":transcript-src":75930},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F02\u002F01\u002Fhow-to-run-and-debug-your-github-workflows-locally\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F02\u002F01\u002Fhow-to-run-and-debug-your-github-workflows-locally\u002Fsummary.json",[27,75932,75933],{},[63,75934],{"alt":12847,"src":75935},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1769425117\u002Fblog\u002Fhow-to-run-and-debug-your-github-workflows-locally\u002Fhow-to-run-and-debug-your-github-workflows-locally_afbogh",[3244,75937,75938],{},[27,75939,75940],{},[42,75941,75942],{},[30,75943,75944],{},"Test your workflows before you push a commit.",[27,75946,75947],{},"If you've ever worked with GitHub Actions, you've probably experienced this loop: make a change to your workflow file, commit it, push it, wait for the runner to pick it up, watch it fail because of a typo, fix it, commit again, push again, wait again…",[27,75949,75950,75951,75954],{},"For something that's supposed to ",[42,75952,75953],{},"automate"," work, that feedback loop can feel painfully slow - especially when the mistake is something trivial like a missing indent or a typo in a step name.",[27,75956,75957,75958,75961],{},"What if you could run your GitHub Actions ",[42,75959,75960],{},"locally",", on your own machine, and catch those problems instantly?",[27,75963,75964,75965,75972],{},"That's exactly what ",[45,75966,75969],{"href":75967,"target":2716,"rel":75968},"https:\u002F\u002Fgithub.com\u002Fnektos\u002Fact",[2718,2719],[42,75970,75971],{},"act"," does.",[27,75974,75975,75977,75978,75981],{},[42,75976,75971],{}," is a small CLI tool that executes GitHub Actions workflows locally using Docker. It reads the same ",[22,75979,75980],{},".github\u002Fworkflows\u002F*.yml"," files you already have and runs them in containers that closely resemble GitHub's hosted runners.",[27,75983,75984,75985,75987,75988,75991],{},"In this article, we'll explain how ",[42,75986,75971],{}," works, then walk through ",[42,75989,75990],{},"practical, real-world examples -"," from simple push triggers to cron jobs, Python and TypeScript workflows, matrix builds, secrets, caching, and multi-job pipelines.",[27,75993,75994],{},"Let's dive in.",[104,75996,75998],{"id":75997},"why-test-github-actions-locally","Why Test GitHub Actions Locally?",[27,76000,76001],{},[63,76002],{"alt":76003,"src":76004},"Why Test GitHub Actions Locally","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1769425116\u002Fblog\u002Fhow-to-run-and-debug-your-github-workflows-locally\u002Fwhy-test-github-actions-locally_yjdtcz",[27,76006,76007],{},"Before we dive into the setup, let's talk about why this matters.",[27,76009,76010],{},"GitHub Actions is powerful. It handles CI\u002FCD, automation, scheduled tasks, and much more. But the feedback loop can be painfully slow. Every time you want to test a change, you need to:",[2569,76012,76013,76016,76019,76022,76025,76028],{},[1006,76014,76015],{},"Commit your changes",[1006,76017,76018],{},"Push to GitHub",[1006,76020,76021],{},"Wait for a runner to become available",[1006,76023,76024],{},"Watch your workflow execute (or fail)",[1006,76026,76027],{},"Read through logs to find the issue",[1006,76029,76030],{},"Repeat",[27,76032,76033,76034,76037,76038,76043],{},"For complex workflows, this cycle can eat up hours of your day. And if you're on a team with limited GitHub Actions minutes (for ",[42,76035,76036],{},"private repositories",", each GitHub account receives ",[45,76039,76042],{"href":76040,"target":2716,"rel":76041},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fbilling\u002Fconcepts\u002Fproduct-billing\u002Fgithub-actions",[2718,2719],"2,000 free minutes per month","), those failed runs add up quickly.",[27,76045,76046,76048,76049,76052],{},[42,76047,75971],{}," solves this by bringing the execution environment to your machine. It reads your ",[22,76050,76051],{},".github\u002Fworkflows\u002F"," directory, pulls the necessary Docker images, and runs your workflows locally - with the same environment variables and filesystem structure that GitHub provides.",[27,76054,76055],{},"The result? You catch issues before they ever reach your repository.",[104,76057,66831],{"id":66828},[27,76059,76060],{},"Before we begin, you'll need three things installed on your machine:",[2569,76062,76063,76068,76077],{},[1006,76064,76065,76067],{},[42,76066,75971],{}," - the CLI tool that lets you run GitHub Actions locally",[1006,76069,76070,76073,76074,76076],{},[42,76071,76072],{},"Docker Desktop"," - ",[42,76075,75971],{}," uses Docker containers to simulate GitHub's runners",[1006,76078,76079,76085,76086,114,76088,76090],{},[45,76080,76083],{"href":76081,"target":2716,"rel":76082},"https:\u002F\u002Fbrew.sh\u002F",[2718,2719],[42,76084,57021],{}," - we'll use it to install ",[42,76087,75971],{},[42,76089,76072],{}," (on macOS or Linux)",[3244,76092,76093],{},[27,76094,76095],{},"On Windows, Homebrew isn't available - you'll need to use a different package manager.",[27,76097,76098],{},"Let's get everything set up.",[123,76100,76102],{"id":76101},"installing-docker-desktop","Installing Docker Desktop",[27,76104,76105],{},"If you don't have Docker installed yet, the easiest way on macOS is through Homebrew:",[128,76107,76109],{"className":8665,"code":76108,"language":8667,"meta":133,"style":133},"brew install --cask docker-desktop\n",[22,76110,76111],{"__ignoreMap":133},[137,76112,76113,76115,76117,76120],{"class":139,"line":140},[137,76114,56827],{"class":147},[137,76116,10268],{"class":284},[137,76118,76119],{"class":364}," --cask",[137,76121,76122],{"class":284}," docker-desktop\n",[27,76124,76125],{},"After installation, open Docker Desktop from your Applications folder. You'll see the Docker whale icon appear in your menu bar once it's running.",[27,76127,76128],{},"To verify Docker is working correctly, run:",[128,76130,76132],{"className":8665,"code":76131,"language":8667,"meta":133,"style":133},"docker --version\n",[22,76133,76134],{"__ignoreMap":133},[137,76135,76136,76139],{"class":139,"line":140},[137,76137,76138],{"class":147},"docker",[137,76140,76141],{"class":364}," --version\n",[27,76143,76144],{},"You should see something like:",[128,76146,76149],{"className":76147,"code":76148,"language":5189},[5187],"Docker version 24.0.6, build ed223bc\n",[22,76150,76148],{"__ignoreMap":133},[3244,76152,76153],{},[27,76154,76155,76156,76158,76159,76161],{},"Make sure Docker Desktop is actually running before you try to use ",[42,76157,75971],{},". The Docker daemon needs to be active for ",[42,76160,75971],{}," to create and manage containers.",[123,76163,76165],{"id":76164},"installing-act","Installing act",[27,76167,76168,76169,76171],{},"With Homebrew ready, installing ",[42,76170,75971],{}," is straightforward:",[128,76173,76175],{"className":8665,"code":76174,"language":8667,"meta":133,"style":133},"brew install act\n",[22,76176,76177],{"__ignoreMap":133},[137,76178,76179,76181,76183],{"class":139,"line":140},[137,76180,56827],{"class":147},[137,76182,10268],{"class":284},[137,76184,76185],{"class":284}," act\n",[27,76187,76188],{},"Verify the installation:",[128,76190,76192],{"className":8665,"code":76191,"language":8667,"meta":133,"style":133},"act --version\n",[22,76193,76194],{"__ignoreMap":133},[137,76195,76196,76198],{"class":139,"line":140},[137,76197,75971],{"class":147},[137,76199,76141],{"class":364},[27,76201,76202],{},"You should see output like:",[128,76204,76207],{"className":76205,"code":76206,"language":5189},[5187],"act version 0.2.84\n",[22,76208,76206],{"__ignoreMap":133},[27,76210,76211],{},"That's it for the prerequisites. You're ready to start running GitHub Actions locally.",[104,76213,76215],{"id":76214},"your-first-local-workflow","Your First Local Workflow",[27,76217,76218,76219,894],{},"Let's start with the basics. Create a simple workflow file at ",[22,76220,76221],{},".github\u002Fworkflows\u002Fsimple-push.yml",[128,76223,76227],{"className":76224,"code":76225,"language":76226,"meta":133,"style":133},"language-yaml shiki shiki-themes github-light github-dark","name: 01 - Simple Push Action\n\non:\n    push:\n        branches:\n            - main\n            - develop\n    pull_request:\n        branches:\n            - main\n\njobs:\n    greeting:\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout code\n              uses: actions\u002Fcheckout@v4\n\n            - name: Say Hello\n              run: echo \"Hello from GitHub Actions!\"\n\n            - name: Show current date\n              run: date\n\n            - name: List files in repository\n              run: ls -la\n\n            - name: Show environment info\n              run: |\n                  echo \"Runner OS: $RUNNER_OS\"\n                  echo \"GitHub Actor: $GITHUB_ACTOR\"\n                  echo \"GitHub Repository: $GITHUB_REPOSITORY\"\n                  echo \"GitHub Event: $GITHUB_EVENT_NAME\"\n","yaml",[22,76228,76229,76238,76242,76248,76255,76262,76270,76277,76284,76290,76296,76300,76307,76314,76324,76331,76342,76352,76356,76367,76377,76381,76392,76401,76405,76416,76425,76429,76440,76449,76454,76459,76464],{"__ignoreMap":133},[137,76230,76231,76233,76235],{"class":139,"line":140},[137,76232,1387],{"class":4036},[137,76234,726],{"class":157},[137,76236,76237],{"class":284},"01 - Simple Push Action\n",[137,76239,76240],{"class":139,"line":173},[137,76241,516],{"emptyLinePlaceholder":515},[137,76243,76244,76246],{"class":139,"line":188},[137,76245,72117],{"class":364},[137,76247,36830],{"class":157},[137,76249,76250,76253],{"class":139,"line":269},[137,76251,76252],{"class":4036},"    push",[137,76254,36830],{"class":157},[137,76256,76257,76260],{"class":139,"line":278},[137,76258,76259],{"class":4036},"        branches",[137,76261,36830],{"class":157},[137,76263,76264,76267],{"class":139,"line":291},[137,76265,76266],{"class":157},"            - ",[137,76268,76269],{"class":284},"main\n",[137,76271,76272,76274],{"class":139,"line":297},[137,76273,76266],{"class":157},[137,76275,76276],{"class":284},"develop\n",[137,76278,76279,76282],{"class":139,"line":302},[137,76280,76281],{"class":4036},"    pull_request",[137,76283,36830],{"class":157},[137,76285,76286,76288],{"class":139,"line":662},[137,76287,76259],{"class":4036},[137,76289,36830],{"class":157},[137,76291,76292,76294],{"class":139,"line":667},[137,76293,76266],{"class":157},[137,76295,76269],{"class":284},[137,76297,76298],{"class":139,"line":786},[137,76299,516],{"emptyLinePlaceholder":515},[137,76301,76302,76305],{"class":139,"line":798},[137,76303,76304],{"class":4036},"jobs",[137,76306,36830],{"class":157},[137,76308,76309,76312],{"class":139,"line":803},[137,76310,76311],{"class":4036},"    greeting",[137,76313,36830],{"class":157},[137,76315,76316,76319,76321],{"class":139,"line":931},[137,76317,76318],{"class":4036},"        runs-on",[137,76320,726],{"class":157},[137,76322,76323],{"class":284},"ubuntu-latest\n",[137,76325,76326,76329],{"class":139,"line":1568},[137,76327,76328],{"class":4036},"        steps",[137,76330,36830],{"class":157},[137,76332,76333,76335,76337,76339],{"class":139,"line":1573},[137,76334,76266],{"class":157},[137,76336,1387],{"class":4036},[137,76338,726],{"class":157},[137,76340,76341],{"class":284},"Checkout code\n",[137,76343,76344,76347,76349],{"class":139,"line":1578},[137,76345,76346],{"class":4036},"              uses",[137,76348,726],{"class":157},[137,76350,76351],{"class":284},"actions\u002Fcheckout@v4\n",[137,76353,76354],{"class":139,"line":1588},[137,76355,516],{"emptyLinePlaceholder":515},[137,76357,76358,76360,76362,76364],{"class":139,"line":1601},[137,76359,76266],{"class":157},[137,76361,1387],{"class":4036},[137,76363,726],{"class":157},[137,76365,76366],{"class":284},"Say Hello\n",[137,76368,76369,76372,76374],{"class":139,"line":3802},[137,76370,76371],{"class":4036},"              run",[137,76373,726],{"class":157},[137,76375,76376],{"class":284},"echo \"Hello from GitHub Actions!\"\n",[137,76378,76379],{"class":139,"line":3808},[137,76380,516],{"emptyLinePlaceholder":515},[137,76382,76383,76385,76387,76389],{"class":139,"line":3822},[137,76384,76266],{"class":157},[137,76386,1387],{"class":4036},[137,76388,726],{"class":157},[137,76390,76391],{"class":284},"Show current date\n",[137,76393,76394,76396,76398],{"class":139,"line":3827},[137,76395,76371],{"class":4036},[137,76397,726],{"class":157},[137,76399,76400],{"class":284},"date\n",[137,76402,76403],{"class":139,"line":3832},[137,76404,516],{"emptyLinePlaceholder":515},[137,76406,76407,76409,76411,76413],{"class":139,"line":3840},[137,76408,76266],{"class":157},[137,76410,1387],{"class":4036},[137,76412,726],{"class":157},[137,76414,76415],{"class":284},"List files in repository\n",[137,76417,76418,76420,76422],{"class":139,"line":3846},[137,76419,76371],{"class":4036},[137,76421,726],{"class":157},[137,76423,76424],{"class":284},"ls -la\n",[137,76426,76427],{"class":139,"line":3861},[137,76428,516],{"emptyLinePlaceholder":515},[137,76430,76431,76433,76435,76437],{"class":139,"line":3883},[137,76432,76266],{"class":157},[137,76434,1387],{"class":4036},[137,76436,726],{"class":157},[137,76438,76439],{"class":284},"Show environment info\n",[137,76441,76442,76444,76446],{"class":139,"line":3896},[137,76443,76371],{"class":4036},[137,76445,726],{"class":157},[137,76447,76448],{"class":143},"|\n",[137,76450,76451],{"class":139,"line":3901},[137,76452,76453],{"class":284},"                  echo \"Runner OS: $RUNNER_OS\"\n",[137,76455,76456],{"class":139,"line":3906},[137,76457,76458],{"class":284},"                  echo \"GitHub Actor: $GITHUB_ACTOR\"\n",[137,76460,76461],{"class":139,"line":3911},[137,76462,76463],{"class":284},"                  echo \"GitHub Repository: $GITHUB_REPOSITORY\"\n",[137,76465,76466],{"class":139,"line":4666},[137,76467,76468],{"class":284},"                  echo \"GitHub Event: $GITHUB_EVENT_NAME\"\n",[27,76470,76471,76472,114,76474,76477],{},"This workflow triggers on ",[22,76473,8583],{},[22,76475,76476],{},"pull request"," events, checks out your code, and displays basic information.",[27,76479,76480],{},"Now for the moment of truth - run the following command in your terminal.",[128,76482,76484],{"className":8665,"code":76483,"language":8667,"meta":133,"style":133},"act push\n",[22,76485,76486],{"__ignoreMap":133},[137,76487,76488,76490],{"class":139,"line":140},[137,76489,75971],{"class":147},[137,76491,76492],{"class":284}," push\n",[27,76494,76495,76496,76498],{},"The first time you run ",[42,76497,75971],{},", it will ask you to choose a default Docker image size:",[1003,76500,76501,76507,76513],{},[1006,76502,76503,76506],{},[42,76504,76505],{},"Micro"," - Lightweight and fast, but with limited tools",[1006,76508,76509,76512],{},[42,76510,76511],{},"Medium"," - A good balance (recommended for most use cases)",[1006,76514,76515,76518],{},[42,76516,76517],{},"Large"," - Full GitHub runner compatibility, but very large (~20GB)",[27,76520,76521,76522,76524],{},"I recommend starting with ",[42,76523,76511],{},". You can always change this later.",[27,76526,76527,76528,76530],{},"After selecting an image, ",[42,76529,75971],{}," will pull the necessary Docker container and execute your workflow. You should see output similar to:",[128,76532,76535],{"className":76533,"code":76534,"language":5189},[5187],"[01 - Simple Push Action\u002Fgreeting] ⭐ Run Set up job\n[01 - Simple Push Action\u002Fgreeting]   ✅  Success - Set up job\n[01 - Simple Push Action\u002Fgreeting] ⭐ Run Main Checkout code\n[01 - Simple Push Action\u002Fgreeting]   ✅  Success - Main Checkout code\n[01 - Simple Push Action\u002Fgreeting] ⭐ Run Main Say Hello\n[01 - Simple Push Action\u002Fgreeting]   | Hello from GitHub Actions!\n[01 - Simple Push Action\u002Fgreeting]   ✅  Success - Main Say Hello\n...\n[01 - Simple Push Action\u002Fgreeting] 🏁  Job succeeded\n",[22,76536,76534],{"__ignoreMap":133},[27,76538,76539],{},"Congratulations! You just ran your first GitHub Actions workflow locally.",[104,76541,76543],{"id":76542},"a-note-for-macos-and-apple-silicon-users","A Note for macOS and Apple Silicon Users",[3244,76545,76546],{},[27,76547,76548],{},"If you're on a Mac with an M1, M2, or M3 chip, you might encounter some issues with Docker containers. The GitHub Actions runners are built for x86_64 architecture, and your Mac uses ARM.",[27,76550,76551,76552,76555],{},"To avoid problems, I recommend creating a ",[22,76553,76554],{},".actrc"," file in your project root with these settings:",[128,76557,76560],{"className":76558,"code":76559,"language":5189},[5187],"# Default act configuration\n\n# Use medium-sized Ubuntu image\n-P ubuntu-latest=catthehacker\u002Fubuntu:act-latest\n-P ubuntu-22.04=catthehacker\u002Fubuntu:act-22.04\n-P ubuntu-20.04=catthehacker\u002Fubuntu:act-20.04\n\n# Enable artifact server for artifact workflows\n--artifact-server-path \u002Ftmp\u002Fartifacts\n\n# macOS \u002F Apple Silicon: Required for Docker Desktop compatibility\n--container-architecture linux\u002Famd64\n--container-daemon-socket=-\n",[22,76561,76559],{"__ignoreMap":133},[27,76563,76564,76565,76567],{},"With this configuration, ",[42,76566,75971],{}," will automatically use the correct architecture and settings every time you run it.",[27,76569,76570],{},"Alternatively, you can pass these flags manually:",[128,76572,76574],{"className":8665,"code":76573,"language":8667,"meta":133,"style":133},"act push --container-architecture linux\u002Famd64 --container-daemon-socket=-\n",[22,76575,76576],{"__ignoreMap":133},[137,76577,76578,76580,76583,76586,76589],{"class":139,"line":140},[137,76579,75971],{"class":147},[137,76581,76582],{"class":284}," push",[137,76584,76585],{"class":364}," --container-architecture",[137,76587,76588],{"class":284}," linux\u002Famd64",[137,76590,76591],{"class":364}," --container-daemon-socket=-\n",[104,76593,76595],{"id":76594},"running-scheduled-workflows-cron-jobs","Running Scheduled Workflows (Cron Jobs)",[27,76597,76598],{},"GitHub Actions supports scheduled workflows using cron expressions. Let's create one.",[27,76600,20297,76601,894],{},[22,76602,76603],{},".github\u002Fworkflows\u002Fcron-schedule.yml",[128,76605,76607],{"className":76224,"code":76606,"language":76226,"meta":133,"style":133},"name: 02 - Cron Scheduled Action\n\non:\n    schedule:\n        # Runs every day at midnight UTC\n        - cron: \"0 0 * * *\"\n        # Runs every Monday at 9am UTC\n        - cron: \"0 9 * * 1\"\n    # Allow manual trigger for testing\n    workflow_dispatch:\n\njobs:\n    scheduled-task:\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout code\n              uses: actions\u002Fcheckout@v4\n\n            - name: Display scheduled run info\n              run: |\n                  echo \"Scheduled job running!\"\n                  echo \"Current time: $(date)\"\n                  echo \"Triggered by: ${{ github.event_name }}\"\n\n            - name: Simulate daily cleanup task\n              run: |\n                  echo \"Performing daily cleanup...\"\n                  echo \"Checking for stale files...\"\n                  echo \"Cleanup complete!\"\n\n            - name: Generate daily report\n              run: |\n                  echo \"=== Daily Report ===\"\n                  echo \"Date: $(date +%Y-%m-%d)\"\n                  echo \"Repository: $GITHUB_REPOSITORY\"\n                  echo \"Branch: $GITHUB_REF\"\n                  echo \"====================\"\n",[22,76608,76609,76618,76622,76628,76635,76640,76653,76658,76669,76674,76681,76685,76691,76698,76706,76712,76722,76730,76734,76745,76753,76758,76763,76768,76772,76783,76791,76796,76801,76806,76810,76821,76829,76834,76839,76844,76849],{"__ignoreMap":133},[137,76610,76611,76613,76615],{"class":139,"line":140},[137,76612,1387],{"class":4036},[137,76614,726],{"class":157},[137,76616,76617],{"class":284},"02 - Cron Scheduled Action\n",[137,76619,76620],{"class":139,"line":173},[137,76621,516],{"emptyLinePlaceholder":515},[137,76623,76624,76626],{"class":139,"line":188},[137,76625,72117],{"class":364},[137,76627,36830],{"class":157},[137,76629,76630,76633],{"class":139,"line":269},[137,76631,76632],{"class":4036},"    schedule",[137,76634,36830],{"class":157},[137,76636,76637],{"class":139,"line":278},[137,76638,76639],{"class":308},"        # Runs every day at midnight UTC\n",[137,76641,76642,76645,76648,76650],{"class":139,"line":291},[137,76643,76644],{"class":157},"        - ",[137,76646,76647],{"class":4036},"cron",[137,76649,726],{"class":157},[137,76651,76652],{"class":284},"\"0 0 * * *\"\n",[137,76654,76655],{"class":139,"line":297},[137,76656,76657],{"class":308},"        # Runs every Monday at 9am UTC\n",[137,76659,76660,76662,76664,76666],{"class":139,"line":302},[137,76661,76644],{"class":157},[137,76663,76647],{"class":4036},[137,76665,726],{"class":157},[137,76667,76668],{"class":284},"\"0 9 * * 1\"\n",[137,76670,76671],{"class":139,"line":662},[137,76672,76673],{"class":308},"    # Allow manual trigger for testing\n",[137,76675,76676,76679],{"class":139,"line":667},[137,76677,76678],{"class":4036},"    workflow_dispatch",[137,76680,36830],{"class":157},[137,76682,76683],{"class":139,"line":786},[137,76684,516],{"emptyLinePlaceholder":515},[137,76686,76687,76689],{"class":139,"line":798},[137,76688,76304],{"class":4036},[137,76690,36830],{"class":157},[137,76692,76693,76696],{"class":139,"line":803},[137,76694,76695],{"class":4036},"    scheduled-task",[137,76697,36830],{"class":157},[137,76699,76700,76702,76704],{"class":139,"line":931},[137,76701,76318],{"class":4036},[137,76703,726],{"class":157},[137,76705,76323],{"class":284},[137,76707,76708,76710],{"class":139,"line":1568},[137,76709,76328],{"class":4036},[137,76711,36830],{"class":157},[137,76713,76714,76716,76718,76720],{"class":139,"line":1573},[137,76715,76266],{"class":157},[137,76717,1387],{"class":4036},[137,76719,726],{"class":157},[137,76721,76341],{"class":284},[137,76723,76724,76726,76728],{"class":139,"line":1578},[137,76725,76346],{"class":4036},[137,76727,726],{"class":157},[137,76729,76351],{"class":284},[137,76731,76732],{"class":139,"line":1588},[137,76733,516],{"emptyLinePlaceholder":515},[137,76735,76736,76738,76740,76742],{"class":139,"line":1601},[137,76737,76266],{"class":157},[137,76739,1387],{"class":4036},[137,76741,726],{"class":157},[137,76743,76744],{"class":284},"Display scheduled run info\n",[137,76746,76747,76749,76751],{"class":139,"line":3802},[137,76748,76371],{"class":4036},[137,76750,726],{"class":157},[137,76752,76448],{"class":143},[137,76754,76755],{"class":139,"line":3808},[137,76756,76757],{"class":284},"                  echo \"Scheduled job running!\"\n",[137,76759,76760],{"class":139,"line":3822},[137,76761,76762],{"class":284},"                  echo \"Current time: $(date)\"\n",[137,76764,76765],{"class":139,"line":3827},[137,76766,76767],{"class":284},"                  echo \"Triggered by: ${{ github.event_name }}\"\n",[137,76769,76770],{"class":139,"line":3832},[137,76771,516],{"emptyLinePlaceholder":515},[137,76773,76774,76776,76778,76780],{"class":139,"line":3840},[137,76775,76266],{"class":157},[137,76777,1387],{"class":4036},[137,76779,726],{"class":157},[137,76781,76782],{"class":284},"Simulate daily cleanup task\n",[137,76784,76785,76787,76789],{"class":139,"line":3846},[137,76786,76371],{"class":4036},[137,76788,726],{"class":157},[137,76790,76448],{"class":143},[137,76792,76793],{"class":139,"line":3861},[137,76794,76795],{"class":284},"                  echo \"Performing daily cleanup...\"\n",[137,76797,76798],{"class":139,"line":3883},[137,76799,76800],{"class":284},"                  echo \"Checking for stale files...\"\n",[137,76802,76803],{"class":139,"line":3896},[137,76804,76805],{"class":284},"                  echo \"Cleanup complete!\"\n",[137,76807,76808],{"class":139,"line":3901},[137,76809,516],{"emptyLinePlaceholder":515},[137,76811,76812,76814,76816,76818],{"class":139,"line":3906},[137,76813,76266],{"class":157},[137,76815,1387],{"class":4036},[137,76817,726],{"class":157},[137,76819,76820],{"class":284},"Generate daily report\n",[137,76822,76823,76825,76827],{"class":139,"line":3911},[137,76824,76371],{"class":4036},[137,76826,726],{"class":157},[137,76828,76448],{"class":143},[137,76830,76831],{"class":139,"line":4666},[137,76832,76833],{"class":284},"                  echo \"=== Daily Report ===\"\n",[137,76835,76836],{"class":139,"line":4672},[137,76837,76838],{"class":284},"                  echo \"Date: $(date +%Y-%m-%d)\"\n",[137,76840,76841],{"class":139,"line":4680},[137,76842,76843],{"class":284},"                  echo \"Repository: $GITHUB_REPOSITORY\"\n",[137,76845,76846],{"class":139,"line":4711},[137,76847,76848],{"class":284},"                  echo \"Branch: $GITHUB_REF\"\n",[137,76850,76851],{"class":139,"line":4716},[137,76852,76853],{"class":284},"                  echo \"====================\"\n",[27,76855,76856,76857,76860],{},"To run this locally, use the ",[22,76858,76859],{},"schedule"," event:",[128,76862,76864],{"className":8665,"code":76863,"language":8667,"meta":133,"style":133},"act schedule -W .github\u002Fworkflows\u002Fcron-schedule.yml\n",[22,76865,76866],{"__ignoreMap":133},[137,76867,76868,76870,76873,76876],{"class":139,"line":140},[137,76869,75971],{"class":147},[137,76871,76872],{"class":284}," schedule",[137,76874,76875],{"class":364}," -W",[137,76877,76878],{"class":284}," .github\u002Fworkflows\u002Fcron-schedule.yml\n",[27,76880,4737,76881,76884],{},[22,76882,76883],{},"-W"," flag specifies which workflow file to run. This is useful when you have multiple workflows and want to test a specific one.",[104,76886,76888],{"id":76887},"running-python-in-github-actions","Running Python in GitHub Actions",[27,76890,76891],{},"Let's get more practical. Here's a workflow that sets up Python, runs a script, and executes inline Python code.",[27,76893,76894,76895,894],{},"First, create a Python script at ",[22,76896,76897],{},"scripts\u002Fhello.py",[128,76899,76901],{"className":28815,"code":76900,"language":28817,"meta":133,"style":133},"#!\u002Fusr\u002Fbin\u002Fenv python3\n\"\"\"\nSimple Python script for GitHub Actions demonstration.\n\"\"\"\n\nimport os\nfrom datetime import datetime\n\ndef main():\n    print(\"=\" * 50)\n    print(\"Hello from Python GitHub Action!\")\n    print(\"=\" * 50)\n    print(f\"Current time: {datetime.now()}\")\n    print(f\"Working directory: {os.getcwd()}\")\n\n    # Show some environment variables if available\n    github_vars = [\n        \"GITHUB_REPOSITORY\",\n        \"GITHUB_ACTOR\",\n        \"GITHUB_EVENT_NAME\",\n        \"GITHUB_REF\",\n    ]\n\n    print(\"\\nGitHub Environment Variables:\")\n    for var in github_vars:\n        value = os.environ.get(var, \"Not set\")\n        print(f\"  {var}: {value}\")\n\n    print(\"=\" * 50)\n    print(\"Python script executed successfully!\")\n    print(\"=\" * 50)\n\nif __name__ == \"__main__\":\n    main()\n",[22,76902,76903,76908,76913,76918,76922,76926,76931,76936,76940,76945,76950,76955,76959,76964,76969,76973,76978,76983,76988,76993,76998,77003,77007,77011,77016,77021,77026,77031,77035,77039,77044,77048,77052,77057],{"__ignoreMap":133},[137,76904,76905],{"class":139,"line":140},[137,76906,76907],{},"#!\u002Fusr\u002Fbin\u002Fenv python3\n",[137,76909,76910],{"class":139,"line":173},[137,76911,76912],{},"\"\"\"\n",[137,76914,76915],{"class":139,"line":188},[137,76916,76917],{},"Simple Python script for GitHub Actions demonstration.\n",[137,76919,76920],{"class":139,"line":269},[137,76921,76912],{},[137,76923,76924],{"class":139,"line":278},[137,76925,516],{"emptyLinePlaceholder":515},[137,76927,76928],{"class":139,"line":291},[137,76929,76930],{},"import os\n",[137,76932,76933],{"class":139,"line":297},[137,76934,76935],{},"from datetime import datetime\n",[137,76937,76938],{"class":139,"line":302},[137,76939,516],{"emptyLinePlaceholder":515},[137,76941,76942],{"class":139,"line":662},[137,76943,76944],{},"def main():\n",[137,76946,76947],{"class":139,"line":667},[137,76948,76949],{},"    print(\"=\" * 50)\n",[137,76951,76952],{"class":139,"line":786},[137,76953,76954],{},"    print(\"Hello from Python GitHub Action!\")\n",[137,76956,76957],{"class":139,"line":798},[137,76958,76949],{},[137,76960,76961],{"class":139,"line":803},[137,76962,76963],{},"    print(f\"Current time: {datetime.now()}\")\n",[137,76965,76966],{"class":139,"line":931},[137,76967,76968],{},"    print(f\"Working directory: {os.getcwd()}\")\n",[137,76970,76971],{"class":139,"line":1568},[137,76972,516],{"emptyLinePlaceholder":515},[137,76974,76975],{"class":139,"line":1573},[137,76976,76977],{},"    # Show some environment variables if available\n",[137,76979,76980],{"class":139,"line":1578},[137,76981,76982],{},"    github_vars = [\n",[137,76984,76985],{"class":139,"line":1588},[137,76986,76987],{},"        \"GITHUB_REPOSITORY\",\n",[137,76989,76990],{"class":139,"line":1601},[137,76991,76992],{},"        \"GITHUB_ACTOR\",\n",[137,76994,76995],{"class":139,"line":3802},[137,76996,76997],{},"        \"GITHUB_EVENT_NAME\",\n",[137,76999,77000],{"class":139,"line":3808},[137,77001,77002],{},"        \"GITHUB_REF\",\n",[137,77004,77005],{"class":139,"line":3822},[137,77006,68301],{},[137,77008,77009],{"class":139,"line":3827},[137,77010,516],{"emptyLinePlaceholder":515},[137,77012,77013],{"class":139,"line":3832},[137,77014,77015],{},"    print(\"\\nGitHub Environment Variables:\")\n",[137,77017,77018],{"class":139,"line":3840},[137,77019,77020],{},"    for var in github_vars:\n",[137,77022,77023],{"class":139,"line":3846},[137,77024,77025],{},"        value = os.environ.get(var, \"Not set\")\n",[137,77027,77028],{"class":139,"line":3861},[137,77029,77030],{},"        print(f\"  {var}: {value}\")\n",[137,77032,77033],{"class":139,"line":3883},[137,77034,516],{"emptyLinePlaceholder":515},[137,77036,77037],{"class":139,"line":3896},[137,77038,76949],{},[137,77040,77041],{"class":139,"line":3901},[137,77042,77043],{},"    print(\"Python script executed successfully!\")\n",[137,77045,77046],{"class":139,"line":3906},[137,77047,76949],{},[137,77049,77050],{"class":139,"line":3911},[137,77051,516],{"emptyLinePlaceholder":515},[137,77053,77054],{"class":139,"line":4666},[137,77055,77056],{},"if __name__ == \"__main__\":\n",[137,77058,77059],{"class":139,"line":4672},[137,77060,77061],{},"    main()\n",[27,77063,77064,77065,894],{},"Now create the workflow at ",[22,77066,77067],{},".github\u002Fworkflows\u002Fpython-action.yml",[128,77069,77071],{"className":76224,"code":77070,"language":76226,"meta":133,"style":133},"name: 03 - Python Action\n\non:\n    push:\n        paths:\n            - \"**.py\"\n            - \".github\u002Fworkflows\u002Fpython-action.yml\"\n    workflow_dispatch:\n\njobs:\n    python-job:\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout code\n              uses: actions\u002Fcheckout@v4\n\n            - name: Set up Python\n              uses: actions\u002Fsetup-python@v5\n              with:\n                  python-version: \"3.12\"\n\n            - name: Display Python version\n              run: python --version\n\n            - name: Run Python script\n              run: python scripts\u002Fhello.py\n\n            - name: Run inline Python code\n              run: |\n                  python \u003C\u003C 'EOF'\n                  import sys\n                  import platform\n                  import datetime\n\n                  print(\"=\" * 50)\n                  print(\"Python Runtime Information\")\n                  print(\"=\" * 50)\n                  print(f\"Python Version: {sys.version}\")\n                  print(f\"Platform: {platform.platform()}\")\n                  print(f\"Current Time: {datetime.datetime.now()}\")\n                  print(\"=\" * 50)\n\n                  # Simple calculation\n                  numbers = [1, 2, 3, 4, 5]\n                  print(f\"Sum of {numbers} = {sum(numbers)}\")\n                  print(f\"Average = {sum(numbers) \u002F len(numbers)}\")\n                  print(\"=\" * 50)\n                  EOF\n",[22,77072,77073,77082,77086,77092,77098,77105,77112,77119,77125,77129,77135,77142,77150,77156,77166,77174,77178,77189,77198,77205,77215,77219,77230,77239,77243,77254,77263,77267,77278,77286,77291,77296,77301,77306,77310,77315,77320,77324,77329,77334,77339,77343,77347,77352,77357,77362,77367,77371],{"__ignoreMap":133},[137,77074,77075,77077,77079],{"class":139,"line":140},[137,77076,1387],{"class":4036},[137,77078,726],{"class":157},[137,77080,77081],{"class":284},"03 - Python Action\n",[137,77083,77084],{"class":139,"line":173},[137,77085,516],{"emptyLinePlaceholder":515},[137,77087,77088,77090],{"class":139,"line":188},[137,77089,72117],{"class":364},[137,77091,36830],{"class":157},[137,77093,77094,77096],{"class":139,"line":269},[137,77095,76252],{"class":4036},[137,77097,36830],{"class":157},[137,77099,77100,77103],{"class":139,"line":278},[137,77101,77102],{"class":4036},"        paths",[137,77104,36830],{"class":157},[137,77106,77107,77109],{"class":139,"line":291},[137,77108,76266],{"class":157},[137,77110,77111],{"class":284},"\"**.py\"\n",[137,77113,77114,77116],{"class":139,"line":297},[137,77115,76266],{"class":157},[137,77117,77118],{"class":284},"\".github\u002Fworkflows\u002Fpython-action.yml\"\n",[137,77120,77121,77123],{"class":139,"line":302},[137,77122,76678],{"class":4036},[137,77124,36830],{"class":157},[137,77126,77127],{"class":139,"line":662},[137,77128,516],{"emptyLinePlaceholder":515},[137,77130,77131,77133],{"class":139,"line":667},[137,77132,76304],{"class":4036},[137,77134,36830],{"class":157},[137,77136,77137,77140],{"class":139,"line":786},[137,77138,77139],{"class":4036},"    python-job",[137,77141,36830],{"class":157},[137,77143,77144,77146,77148],{"class":139,"line":798},[137,77145,76318],{"class":4036},[137,77147,726],{"class":157},[137,77149,76323],{"class":284},[137,77151,77152,77154],{"class":139,"line":803},[137,77153,76328],{"class":4036},[137,77155,36830],{"class":157},[137,77157,77158,77160,77162,77164],{"class":139,"line":931},[137,77159,76266],{"class":157},[137,77161,1387],{"class":4036},[137,77163,726],{"class":157},[137,77165,76341],{"class":284},[137,77167,77168,77170,77172],{"class":139,"line":1568},[137,77169,76346],{"class":4036},[137,77171,726],{"class":157},[137,77173,76351],{"class":284},[137,77175,77176],{"class":139,"line":1573},[137,77177,516],{"emptyLinePlaceholder":515},[137,77179,77180,77182,77184,77186],{"class":139,"line":1578},[137,77181,76266],{"class":157},[137,77183,1387],{"class":4036},[137,77185,726],{"class":157},[137,77187,77188],{"class":284},"Set up Python\n",[137,77190,77191,77193,77195],{"class":139,"line":1588},[137,77192,76346],{"class":4036},[137,77194,726],{"class":157},[137,77196,77197],{"class":284},"actions\u002Fsetup-python@v5\n",[137,77199,77200,77203],{"class":139,"line":1601},[137,77201,77202],{"class":4036},"              with",[137,77204,36830],{"class":157},[137,77206,77207,77210,77212],{"class":139,"line":3802},[137,77208,77209],{"class":4036},"                  python-version",[137,77211,726],{"class":157},[137,77213,77214],{"class":284},"\"3.12\"\n",[137,77216,77217],{"class":139,"line":3808},[137,77218,516],{"emptyLinePlaceholder":515},[137,77220,77221,77223,77225,77227],{"class":139,"line":3822},[137,77222,76266],{"class":157},[137,77224,1387],{"class":4036},[137,77226,726],{"class":157},[137,77228,77229],{"class":284},"Display Python version\n",[137,77231,77232,77234,77236],{"class":139,"line":3827},[137,77233,76371],{"class":4036},[137,77235,726],{"class":157},[137,77237,77238],{"class":284},"python --version\n",[137,77240,77241],{"class":139,"line":3832},[137,77242,516],{"emptyLinePlaceholder":515},[137,77244,77245,77247,77249,77251],{"class":139,"line":3840},[137,77246,76266],{"class":157},[137,77248,1387],{"class":4036},[137,77250,726],{"class":157},[137,77252,77253],{"class":284},"Run Python script\n",[137,77255,77256,77258,77260],{"class":139,"line":3846},[137,77257,76371],{"class":4036},[137,77259,726],{"class":157},[137,77261,77262],{"class":284},"python scripts\u002Fhello.py\n",[137,77264,77265],{"class":139,"line":3861},[137,77266,516],{"emptyLinePlaceholder":515},[137,77268,77269,77271,77273,77275],{"class":139,"line":3883},[137,77270,76266],{"class":157},[137,77272,1387],{"class":4036},[137,77274,726],{"class":157},[137,77276,77277],{"class":284},"Run inline Python code\n",[137,77279,77280,77282,77284],{"class":139,"line":3896},[137,77281,76371],{"class":4036},[137,77283,726],{"class":157},[137,77285,76448],{"class":143},[137,77287,77288],{"class":139,"line":3901},[137,77289,77290],{"class":284},"                  python \u003C\u003C 'EOF'\n",[137,77292,77293],{"class":139,"line":3906},[137,77294,77295],{"class":284},"                  import sys\n",[137,77297,77298],{"class":139,"line":3911},[137,77299,77300],{"class":284},"                  import platform\n",[137,77302,77303],{"class":139,"line":4666},[137,77304,77305],{"class":284},"                  import datetime\n",[137,77307,77308],{"class":139,"line":4672},[137,77309,516],{"emptyLinePlaceholder":515},[137,77311,77312],{"class":139,"line":4680},[137,77313,77314],{"class":284},"                  print(\"=\" * 50)\n",[137,77316,77317],{"class":139,"line":4711},[137,77318,77319],{"class":284},"                  print(\"Python Runtime Information\")\n",[137,77321,77322],{"class":139,"line":4716},[137,77323,77314],{"class":284},[137,77325,77326],{"class":139,"line":4721},[137,77327,77328],{"class":284},"                  print(f\"Python Version: {sys.version}\")\n",[137,77330,77331],{"class":139,"line":4727},[137,77332,77333],{"class":284},"                  print(f\"Platform: {platform.platform()}\")\n",[137,77335,77336],{"class":139,"line":4732},[137,77337,77338],{"class":284},"                  print(f\"Current Time: {datetime.datetime.now()}\")\n",[137,77340,77341],{"class":139,"line":5006},[137,77342,77314],{"class":284},[137,77344,77345],{"class":139,"line":5014},[137,77346,516],{"emptyLinePlaceholder":515},[137,77348,77349],{"class":139,"line":14343},[137,77350,77351],{"class":284},"                  # Simple calculation\n",[137,77353,77354],{"class":139,"line":24199},[137,77355,77356],{"class":284},"                  numbers = [1, 2, 3, 4, 5]\n",[137,77358,77359],{"class":139,"line":24773},[137,77360,77361],{"class":284},"                  print(f\"Sum of {numbers} = {sum(numbers)}\")\n",[137,77363,77364],{"class":139,"line":24778},[137,77365,77366],{"class":284},"                  print(f\"Average = {sum(numbers) \u002F len(numbers)}\")\n",[137,77368,77369],{"class":139,"line":24783},[137,77370,77314],{"class":284},[137,77372,77373],{"class":139,"line":24793},[137,77374,77375],{"class":284},"                  EOF\n",[27,77377,77378],{},"Run it:",[128,77380,77382],{"className":8665,"code":77381,"language":8667,"meta":133,"style":133},"act push -W .github\u002Fworkflows\u002Fpython-action.yml -j python-job\n",[22,77383,77384],{"__ignoreMap":133},[137,77385,77386,77388,77390,77392,77395,77398],{"class":139,"line":140},[137,77387,75971],{"class":147},[137,77389,76582],{"class":284},[137,77391,76875],{"class":364},[137,77393,77394],{"class":284}," .github\u002Fworkflows\u002Fpython-action.yml",[137,77396,77397],{"class":364}," -j",[137,77399,77400],{"class":284}," python-job\n",[27,77402,4737,77403,77406],{},[22,77404,77405],{},"-j"," flag lets you run a specific job within a workflow. This is handy when your workflow has multiple jobs and you only want to test one.",[104,77408,77410],{"id":77409},"running-typescript-in-github-actions","Running TypeScript in GitHub Actions",[27,77412,77413,77414,77416],{},"TypeScript workflows are similar, but we need to set up Node.js and install a TypeScript runner. I recommend using ",[22,77415,29200],{}," as it handles ESM modules gracefully.",[27,77418,20297,77419,894],{},[22,77420,77421],{},"scripts\u002Fhello.ts",[128,77423,77425],{"className":13299,"code":77424,"language":13301,"meta":133,"style":133},"#!\u002Fusr\u002Fbin\u002Fenv ts-node\n\u002F**\n * Simple TypeScript script for GitHub Actions demonstration.\n *\u002F\n\ninterface GitHubEnvVars {\n    repository: string;\n    actor: string;\n    eventName: string;\n    ref: string;\n}\n\nfunction getGitHubEnvVars(): GitHubEnvVars {\n    return {\n        repository: process.env.GITHUB_REPOSITORY || \"Not set\",\n        actor: process.env.GITHUB_ACTOR || \"Not set\",\n        eventName: process.env.GITHUB_EVENT_NAME || \"Not set\",\n        ref: process.env.GITHUB_REF || \"Not set\",\n    };\n}\n\nfunction main(): void {\n    console.log(\"=\".repeat(50));\n    console.log(\"Hello from TypeScript GitHub Action!\");\n    console.log(\"=\".repeat(50));\n    console.log(`Current time: ${new Date().toISOString()}`);\n    console.log(`Working directory: ${process.cwd()}`);\n\n    const envVars = getGitHubEnvVars();\n\n    console.log(\"\\nGitHub Environment Variables:\");\n    console.log(`  GITHUB_REPOSITORY: ${envVars.repository}`);\n    console.log(`  GITHUB_ACTOR: ${envVars.actor}`);\n    console.log(`  GITHUB_EVENT_NAME: ${envVars.eventName}`);\n    console.log(`  GITHUB_REF: ${envVars.ref}`);\n\n    console.log(\"=\".repeat(50));\n    console.log(\"TypeScript script executed successfully!\");\n    console.log(\"=\".repeat(50));\n}\n\nmain();\n",[22,77426,77427,77432,77437,77442,77447,77451,77460,77471,77482,77493,77504,77508,77512,77527,77533,77548,77562,77576,77590,77594,77598,77602,77616,77638,77651,77671,77698,77723,77727,77740,77744,77761,77784,77806,77828,77849,77853,77873,77886,77906,77910,77914],{"__ignoreMap":133},[137,77428,77429],{"class":139,"line":140},[137,77430,77431],{"class":308},"#!\u002Fusr\u002Fbin\u002Fenv ts-node\n",[137,77433,77434],{"class":139,"line":173},[137,77435,77436],{"class":308},"\u002F**\n",[137,77438,77439],{"class":139,"line":188},[137,77440,77441],{"class":308}," * Simple TypeScript script for GitHub Actions demonstration.\n",[137,77443,77444],{"class":139,"line":269},[137,77445,77446],{"class":308}," *\u002F\n",[137,77448,77449],{"class":139,"line":278},[137,77450,516],{"emptyLinePlaceholder":515},[137,77452,77453,77455,77458],{"class":139,"line":291},[137,77454,48479],{"class":143},[137,77456,77457],{"class":147}," GitHubEnvVars",[137,77459,256],{"class":157},[137,77461,77462,77465,77467,77469],{"class":139,"line":297},[137,77463,77464],{"class":161},"    repository",[137,77466,894],{"class":143},[137,77468,13630],{"class":364},[137,77470,3276],{"class":157},[137,77472,77473,77476,77478,77480],{"class":139,"line":302},[137,77474,77475],{"class":161},"    actor",[137,77477,894],{"class":143},[137,77479,13630],{"class":364},[137,77481,3276],{"class":157},[137,77483,77484,77487,77489,77491],{"class":139,"line":662},[137,77485,77486],{"class":161},"    eventName",[137,77488,894],{"class":143},[137,77490,13630],{"class":364},[137,77492,3276],{"class":157},[137,77494,77495,77498,77500,77502],{"class":139,"line":667},[137,77496,77497],{"class":161},"    ref",[137,77499,894],{"class":143},[137,77501,13630],{"class":364},[137,77503,3276],{"class":157},[137,77505,77506],{"class":139,"line":786},[137,77507,510],{"class":157},[137,77509,77510],{"class":139,"line":798},[137,77511,516],{"emptyLinePlaceholder":515},[137,77513,77514,77516,77519,77521,77523,77525],{"class":139,"line":803},[137,77515,483],{"class":143},[137,77517,77518],{"class":147}," getGitHubEnvVars",[137,77520,61773],{"class":157},[137,77522,894],{"class":143},[137,77524,77457],{"class":147},[137,77526,256],{"class":157},[137,77528,77529,77531],{"class":139,"line":931},[137,77530,176],{"class":143},[137,77532,256],{"class":157},[137,77534,77535,77538,77541,77543,77546],{"class":139,"line":1568},[137,77536,77537],{"class":157},"        repository: process.env.",[137,77539,77540],{"class":364},"GITHUB_REPOSITORY",[137,77542,24707],{"class":143},[137,77544,77545],{"class":284}," \"Not set\"",[137,77547,1961],{"class":157},[137,77549,77550,77553,77556,77558,77560],{"class":139,"line":1573},[137,77551,77552],{"class":157},"        actor: process.env.",[137,77554,77555],{"class":364},"GITHUB_ACTOR",[137,77557,24707],{"class":143},[137,77559,77545],{"class":284},[137,77561,1961],{"class":157},[137,77563,77564,77567,77570,77572,77574],{"class":139,"line":1578},[137,77565,77566],{"class":157},"        eventName: process.env.",[137,77568,77569],{"class":364},"GITHUB_EVENT_NAME",[137,77571,24707],{"class":143},[137,77573,77545],{"class":284},[137,77575,1961],{"class":157},[137,77577,77578,77581,77584,77586,77588],{"class":139,"line":1588},[137,77579,77580],{"class":157},"        ref: process.env.",[137,77582,77583],{"class":364},"GITHUB_REF",[137,77585,24707],{"class":143},[137,77587,77545],{"class":284},[137,77589,1961],{"class":157},[137,77591,77592],{"class":139,"line":1601},[137,77593,1892],{"class":157},[137,77595,77596],{"class":139,"line":3802},[137,77597,510],{"class":157},[137,77599,77600],{"class":139,"line":3808},[137,77601,516],{"emptyLinePlaceholder":515},[137,77603,77604,77606,77608,77610,77612,77614],{"class":139,"line":3822},[137,77605,483],{"class":143},[137,77607,58463],{"class":147},[137,77609,61773],{"class":157},[137,77611,894],{"class":143},[137,77613,35793],{"class":364},[137,77615,256],{"class":157},[137,77617,77618,77620,77622,77624,77626,77628,77631,77633,77636],{"class":139,"line":3827},[137,77619,493],{"class":157},[137,77621,353],{"class":147},[137,77623,356],{"class":157},[137,77625,58731],{"class":284},[137,77627,1017],{"class":157},[137,77629,77630],{"class":147},"repeat",[137,77632,356],{"class":157},[137,77634,77635],{"class":364},"50",[137,77637,8614],{"class":157},[137,77639,77640,77642,77644,77646,77649],{"class":139,"line":3832},[137,77641,493],{"class":157},[137,77643,353],{"class":147},[137,77645,356],{"class":157},[137,77647,77648],{"class":284},"\"Hello from TypeScript GitHub Action!\"",[137,77650,1502],{"class":157},[137,77652,77653,77655,77657,77659,77661,77663,77665,77667,77669],{"class":139,"line":3840},[137,77654,493],{"class":157},[137,77656,353],{"class":147},[137,77658,356],{"class":157},[137,77660,58731],{"class":284},[137,77662,1017],{"class":157},[137,77664,77630],{"class":147},[137,77666,356],{"class":157},[137,77668,77635],{"class":364},[137,77670,8614],{"class":157},[137,77672,77673,77675,77677,77679,77682,77684,77687,77689,77692,77694,77696],{"class":139,"line":3846},[137,77674,493],{"class":157},[137,77676,353],{"class":147},[137,77678,356],{"class":157},[137,77680,77681],{"class":284},"`Current time: ${",[137,77683,1361],{"class":143},[137,77685,77686],{"class":147}," Date",[137,77688,17766],{"class":284},[137,77690,77691],{"class":147},"toISOString",[137,77693,61773],{"class":284},[137,77695,4706],{"class":284},[137,77697,1502],{"class":157},[137,77699,77700,77702,77704,77706,77709,77712,77714,77717,77719,77721],{"class":139,"line":3861},[137,77701,493],{"class":157},[137,77703,353],{"class":147},[137,77705,356],{"class":157},[137,77707,77708],{"class":284},"`Working directory: ${",[137,77710,77711],{"class":157},"process",[137,77713,1017],{"class":284},[137,77715,77716],{"class":147},"cwd",[137,77718,61773],{"class":284},[137,77720,4706],{"class":284},[137,77722,1502],{"class":157},[137,77724,77725],{"class":139,"line":3883},[137,77726,516],{"emptyLinePlaceholder":515},[137,77728,77729,77731,77734,77736,77738],{"class":139,"line":3896},[137,77730,4177],{"class":143},[137,77732,77733],{"class":364}," envVars",[137,77735,151],{"class":143},[137,77737,77518],{"class":147},[137,77739,924],{"class":157},[137,77741,77742],{"class":139,"line":3901},[137,77743,516],{"emptyLinePlaceholder":515},[137,77745,77746,77748,77750,77752,77754,77756,77759],{"class":139,"line":3906},[137,77747,493],{"class":157},[137,77749,353],{"class":147},[137,77751,356],{"class":157},[137,77753,28792],{"class":284},[137,77755,49636],{"class":364},[137,77757,77758],{"class":284},"GitHub Environment Variables:\"",[137,77760,1502],{"class":157},[137,77762,77763,77765,77767,77769,77772,77775,77777,77780,77782],{"class":139,"line":3911},[137,77764,493],{"class":157},[137,77766,353],{"class":147},[137,77768,356],{"class":157},[137,77770,77771],{"class":284},"`  GITHUB_REPOSITORY: ${",[137,77773,77774],{"class":157},"envVars",[137,77776,1017],{"class":284},[137,77778,77779],{"class":157},"repository",[137,77781,4706],{"class":284},[137,77783,1502],{"class":157},[137,77785,77786,77788,77790,77792,77795,77797,77799,77802,77804],{"class":139,"line":4666},[137,77787,493],{"class":157},[137,77789,353],{"class":147},[137,77791,356],{"class":157},[137,77793,77794],{"class":284},"`  GITHUB_ACTOR: ${",[137,77796,77774],{"class":157},[137,77798,1017],{"class":284},[137,77800,77801],{"class":157},"actor",[137,77803,4706],{"class":284},[137,77805,1502],{"class":157},[137,77807,77808,77810,77812,77814,77817,77819,77821,77824,77826],{"class":139,"line":4672},[137,77809,493],{"class":157},[137,77811,353],{"class":147},[137,77813,356],{"class":157},[137,77815,77816],{"class":284},"`  GITHUB_EVENT_NAME: ${",[137,77818,77774],{"class":157},[137,77820,1017],{"class":284},[137,77822,77823],{"class":157},"eventName",[137,77825,4706],{"class":284},[137,77827,1502],{"class":157},[137,77829,77830,77832,77834,77836,77839,77841,77843,77845,77847],{"class":139,"line":4680},[137,77831,493],{"class":157},[137,77833,353],{"class":147},[137,77835,356],{"class":157},[137,77837,77838],{"class":284},"`  GITHUB_REF: ${",[137,77840,77774],{"class":157},[137,77842,1017],{"class":284},[137,77844,27815],{"class":157},[137,77846,4706],{"class":284},[137,77848,1502],{"class":157},[137,77850,77851],{"class":139,"line":4711},[137,77852,516],{"emptyLinePlaceholder":515},[137,77854,77855,77857,77859,77861,77863,77865,77867,77869,77871],{"class":139,"line":4716},[137,77856,493],{"class":157},[137,77858,353],{"class":147},[137,77860,356],{"class":157},[137,77862,58731],{"class":284},[137,77864,1017],{"class":157},[137,77866,77630],{"class":147},[137,77868,356],{"class":157},[137,77870,77635],{"class":364},[137,77872,8614],{"class":157},[137,77874,77875,77877,77879,77881,77884],{"class":139,"line":4721},[137,77876,493],{"class":157},[137,77878,353],{"class":147},[137,77880,356],{"class":157},[137,77882,77883],{"class":284},"\"TypeScript script executed successfully!\"",[137,77885,1502],{"class":157},[137,77887,77888,77890,77892,77894,77896,77898,77900,77902,77904],{"class":139,"line":4727},[137,77889,493],{"class":157},[137,77891,353],{"class":147},[137,77893,356],{"class":157},[137,77895,58731],{"class":284},[137,77897,1017],{"class":157},[137,77899,77630],{"class":147},[137,77901,356],{"class":157},[137,77903,77635],{"class":364},[137,77905,8614],{"class":157},[137,77907,77908],{"class":139,"line":4732},[137,77909,510],{"class":157},[137,77911,77912],{"class":139,"line":5006},[137,77913,516],{"emptyLinePlaceholder":515},[137,77915,77916,77918],{"class":139,"line":5014},[137,77917,58927],{"class":147},[137,77919,924],{"class":157},[27,77921,77922,77923,894],{},"And the workflow at ",[22,77924,77925],{},".github\u002Fworkflows\u002Ftypescript-action.yml",[128,77927,77929],{"className":76224,"code":77928,"language":76226,"meta":133,"style":133},"name: 04 - TypeScript Action\n\non:\n    push:\n        paths:\n            - \"**.ts\"\n            - \"**.js\"\n            - \".github\u002Fworkflows\u002Ftypescript-action.yml\"\n    workflow_dispatch:\n\njobs:\n    typescript-job:\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout code\n              uses: actions\u002Fcheckout@v4\n\n            - name: Set up Node.js\n              uses: actions\u002Fsetup-node@v4\n              with:\n                  node-version: \"20\"\n\n            - name: Display Node.js version\n              run: node --version\n\n            - name: Install TypeScript\n              run: npm install -g typescript tsx\n\n            - name: Run TypeScript script\n              run: npx tsx scripts\u002Fhello.ts\n",[22,77930,77931,77940,77944,77950,77956,77962,77969,77976,77983,77989,77993,77999,78006,78014,78020,78030,78038,78042,78053,78062,78068,78078,78082,78093,78102,78106,78117,78126,78130,78141],{"__ignoreMap":133},[137,77932,77933,77935,77937],{"class":139,"line":140},[137,77934,1387],{"class":4036},[137,77936,726],{"class":157},[137,77938,77939],{"class":284},"04 - TypeScript Action\n",[137,77941,77942],{"class":139,"line":173},[137,77943,516],{"emptyLinePlaceholder":515},[137,77945,77946,77948],{"class":139,"line":188},[137,77947,72117],{"class":364},[137,77949,36830],{"class":157},[137,77951,77952,77954],{"class":139,"line":269},[137,77953,76252],{"class":4036},[137,77955,36830],{"class":157},[137,77957,77958,77960],{"class":139,"line":278},[137,77959,77102],{"class":4036},[137,77961,36830],{"class":157},[137,77963,77964,77966],{"class":139,"line":291},[137,77965,76266],{"class":157},[137,77967,77968],{"class":284},"\"**.ts\"\n",[137,77970,77971,77973],{"class":139,"line":297},[137,77972,76266],{"class":157},[137,77974,77975],{"class":284},"\"**.js\"\n",[137,77977,77978,77980],{"class":139,"line":302},[137,77979,76266],{"class":157},[137,77981,77982],{"class":284},"\".github\u002Fworkflows\u002Ftypescript-action.yml\"\n",[137,77984,77985,77987],{"class":139,"line":662},[137,77986,76678],{"class":4036},[137,77988,36830],{"class":157},[137,77990,77991],{"class":139,"line":667},[137,77992,516],{"emptyLinePlaceholder":515},[137,77994,77995,77997],{"class":139,"line":786},[137,77996,76304],{"class":4036},[137,77998,36830],{"class":157},[137,78000,78001,78004],{"class":139,"line":798},[137,78002,78003],{"class":4036},"    typescript-job",[137,78005,36830],{"class":157},[137,78007,78008,78010,78012],{"class":139,"line":803},[137,78009,76318],{"class":4036},[137,78011,726],{"class":157},[137,78013,76323],{"class":284},[137,78015,78016,78018],{"class":139,"line":931},[137,78017,76328],{"class":4036},[137,78019,36830],{"class":157},[137,78021,78022,78024,78026,78028],{"class":139,"line":1568},[137,78023,76266],{"class":157},[137,78025,1387],{"class":4036},[137,78027,726],{"class":157},[137,78029,76341],{"class":284},[137,78031,78032,78034,78036],{"class":139,"line":1573},[137,78033,76346],{"class":4036},[137,78035,726],{"class":157},[137,78037,76351],{"class":284},[137,78039,78040],{"class":139,"line":1578},[137,78041,516],{"emptyLinePlaceholder":515},[137,78043,78044,78046,78048,78050],{"class":139,"line":1588},[137,78045,76266],{"class":157},[137,78047,1387],{"class":4036},[137,78049,726],{"class":157},[137,78051,78052],{"class":284},"Set up Node.js\n",[137,78054,78055,78057,78059],{"class":139,"line":1601},[137,78056,76346],{"class":4036},[137,78058,726],{"class":157},[137,78060,78061],{"class":284},"actions\u002Fsetup-node@v4\n",[137,78063,78064,78066],{"class":139,"line":3802},[137,78065,77202],{"class":4036},[137,78067,36830],{"class":157},[137,78069,78070,78073,78075],{"class":139,"line":3808},[137,78071,78072],{"class":4036},"                  node-version",[137,78074,726],{"class":157},[137,78076,78077],{"class":284},"\"20\"\n",[137,78079,78080],{"class":139,"line":3822},[137,78081,516],{"emptyLinePlaceholder":515},[137,78083,78084,78086,78088,78090],{"class":139,"line":3827},[137,78085,76266],{"class":157},[137,78087,1387],{"class":4036},[137,78089,726],{"class":157},[137,78091,78092],{"class":284},"Display Node.js version\n",[137,78094,78095,78097,78099],{"class":139,"line":3832},[137,78096,76371],{"class":4036},[137,78098,726],{"class":157},[137,78100,78101],{"class":284},"node --version\n",[137,78103,78104],{"class":139,"line":3840},[137,78105,516],{"emptyLinePlaceholder":515},[137,78107,78108,78110,78112,78114],{"class":139,"line":3846},[137,78109,76266],{"class":157},[137,78111,1387],{"class":4036},[137,78113,726],{"class":157},[137,78115,78116],{"class":284},"Install TypeScript\n",[137,78118,78119,78121,78123],{"class":139,"line":3861},[137,78120,76371],{"class":4036},[137,78122,726],{"class":157},[137,78124,78125],{"class":284},"npm install -g typescript tsx\n",[137,78127,78128],{"class":139,"line":3883},[137,78129,516],{"emptyLinePlaceholder":515},[137,78131,78132,78134,78136,78138],{"class":139,"line":3896},[137,78133,76266],{"class":157},[137,78135,1387],{"class":4036},[137,78137,726],{"class":157},[137,78139,78140],{"class":284},"Run TypeScript script\n",[137,78142,78143,78145,78147],{"class":139,"line":3901},[137,78144,76371],{"class":4036},[137,78146,726],{"class":157},[137,78148,78149],{"class":284},"npx tsx scripts\u002Fhello.ts\n",[27,78151,77378],{},[128,78153,78155],{"className":8665,"code":78154,"language":8667,"meta":133,"style":133},"act push -W .github\u002Fworkflows\u002Ftypescript-action.yml -j typescript-job\n",[22,78156,78157],{"__ignoreMap":133},[137,78158,78159,78161,78163,78165,78168,78170],{"class":139,"line":140},[137,78160,75971],{"class":147},[137,78162,76582],{"class":284},[137,78164,76875],{"class":364},[137,78166,78167],{"class":284}," .github\u002Fworkflows\u002Ftypescript-action.yml",[137,78169,77397],{"class":364},[137,78171,78172],{"class":284}," typescript-job\n",[104,78174,78176],{"id":78175},"manual-triggers-with-input-parameters","Manual Triggers with Input Parameters",[27,78178,78179],{},"Sometimes you want to trigger a workflow manually with custom inputs - like deploying to a specific environment or running with debug mode enabled.",[27,78181,20297,78182,894],{},[22,78183,78184],{},".github\u002Fworkflows\u002Fworkflow-dispatch.yml",[128,78186,78188],{"className":76224,"code":78187,"language":76226,"meta":133,"style":133},"name: 05 - Manual Trigger (workflow_dispatch)\n\non:\n    workflow_dispatch:\n        inputs:\n            environment:\n                description: \"Deployment environment\"\n                required: true\n                default: \"staging\"\n                type: choice\n                options:\n                    - development\n                    - staging\n                    - production\n            log_level:\n                description: \"Log level\"\n                required: false\n                default: \"info\"\n                type: choice\n                options:\n                    - debug\n                    - info\n                    - warning\n                    - error\n            dry_run:\n                description: \"Perform a dry run without making changes\"\n                required: false\n                default: true\n                type: boolean\n            version:\n                description: \"Version to deploy (e.g., v1.0.0)\"\n                required: false\n                type: string\n\njobs:\n    deploy:\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout code\n              uses: actions\u002Fcheckout@v4\n\n            - name: Display inputs\n              run: |\n                  echo \"=== Workflow Dispatch Inputs ===\"\n                  echo \"Environment: ${{ inputs.environment }}\"\n                  echo \"Log Level: ${{ inputs.log_level }}\"\n                  echo \"Dry Run: ${{ inputs.dry_run }}\"\n                  echo \"Version: ${{ inputs.version || 'latest' }}\"\n                  echo \"================================\"\n\n            - name: Simulate deployment\n              run: |\n                  echo \"Starting deployment process...\"\n                  echo \"Target environment: ${{ inputs.environment }}\"\n\n                  if [ \"${{ inputs.dry_run }}\" = \"true\" ]; then\n                    echo \"[DRY RUN] Would deploy version ${{ inputs.version || 'latest' }} to ${{ inputs.environment }}\"\n                  else\n                    echo \"Deploying version ${{ inputs.version || 'latest' }} to ${{ inputs.environment }}\"\n                  fi\n\n                  echo \"Deployment simulation complete!\"\n",[22,78189,78190,78199,78203,78209,78215,78222,78229,78239,78249,78259,78269,78276,78284,78291,78298,78305,78314,78323,78332,78340,78346,78353,78360,78367,78374,78381,78390,78398,78406,78415,78422,78431,78439,78448,78452,78458,78465,78473,78479,78489,78497,78501,78512,78520,78525,78530,78535,78540,78545,78550,78554,78565,78573,78578,78583,78587,78592,78597,78602,78607,78612,78616],{"__ignoreMap":133},[137,78191,78192,78194,78196],{"class":139,"line":140},[137,78193,1387],{"class":4036},[137,78195,726],{"class":157},[137,78197,78198],{"class":284},"05 - Manual Trigger (workflow_dispatch)\n",[137,78200,78201],{"class":139,"line":173},[137,78202,516],{"emptyLinePlaceholder":515},[137,78204,78205,78207],{"class":139,"line":188},[137,78206,72117],{"class":364},[137,78208,36830],{"class":157},[137,78210,78211,78213],{"class":139,"line":269},[137,78212,76678],{"class":4036},[137,78214,36830],{"class":157},[137,78216,78217,78220],{"class":139,"line":278},[137,78218,78219],{"class":4036},"        inputs",[137,78221,36830],{"class":157},[137,78223,78224,78227],{"class":139,"line":291},[137,78225,78226],{"class":4036},"            environment",[137,78228,36830],{"class":157},[137,78230,78231,78234,78236],{"class":139,"line":297},[137,78232,78233],{"class":4036},"                description",[137,78235,726],{"class":157},[137,78237,78238],{"class":284},"\"Deployment environment\"\n",[137,78240,78241,78244,78246],{"class":139,"line":302},[137,78242,78243],{"class":4036},"                required",[137,78245,726],{"class":157},[137,78247,78248],{"class":364},"true\n",[137,78250,78251,78254,78256],{"class":139,"line":662},[137,78252,78253],{"class":4036},"                default",[137,78255,726],{"class":157},[137,78257,78258],{"class":284},"\"staging\"\n",[137,78260,78261,78264,78266],{"class":139,"line":667},[137,78262,78263],{"class":4036},"                type",[137,78265,726],{"class":157},[137,78267,78268],{"class":284},"choice\n",[137,78270,78271,78274],{"class":139,"line":786},[137,78272,78273],{"class":4036},"                options",[137,78275,36830],{"class":157},[137,78277,78278,78281],{"class":139,"line":798},[137,78279,78280],{"class":157},"                    - ",[137,78282,78283],{"class":284},"development\n",[137,78285,78286,78288],{"class":139,"line":803},[137,78287,78280],{"class":157},[137,78289,78290],{"class":284},"staging\n",[137,78292,78293,78295],{"class":139,"line":931},[137,78294,78280],{"class":157},[137,78296,78297],{"class":284},"production\n",[137,78299,78300,78303],{"class":139,"line":1568},[137,78301,78302],{"class":4036},"            log_level",[137,78304,36830],{"class":157},[137,78306,78307,78309,78311],{"class":139,"line":1573},[137,78308,78233],{"class":4036},[137,78310,726],{"class":157},[137,78312,78313],{"class":284},"\"Log level\"\n",[137,78315,78316,78318,78320],{"class":139,"line":1578},[137,78317,78243],{"class":4036},[137,78319,726],{"class":157},[137,78321,78322],{"class":364},"false\n",[137,78324,78325,78327,78329],{"class":139,"line":1588},[137,78326,78253],{"class":4036},[137,78328,726],{"class":157},[137,78330,78331],{"class":284},"\"info\"\n",[137,78333,78334,78336,78338],{"class":139,"line":1601},[137,78335,78263],{"class":4036},[137,78337,726],{"class":157},[137,78339,78268],{"class":284},[137,78341,78342,78344],{"class":139,"line":3802},[137,78343,78273],{"class":4036},[137,78345,36830],{"class":157},[137,78347,78348,78350],{"class":139,"line":3808},[137,78349,78280],{"class":157},[137,78351,78352],{"class":284},"debug\n",[137,78354,78355,78357],{"class":139,"line":3822},[137,78356,78280],{"class":157},[137,78358,78359],{"class":284},"info\n",[137,78361,78362,78364],{"class":139,"line":3827},[137,78363,78280],{"class":157},[137,78365,78366],{"class":284},"warning\n",[137,78368,78369,78371],{"class":139,"line":3832},[137,78370,78280],{"class":157},[137,78372,78373],{"class":284},"error\n",[137,78375,78376,78379],{"class":139,"line":3840},[137,78377,78378],{"class":4036},"            dry_run",[137,78380,36830],{"class":157},[137,78382,78383,78385,78387],{"class":139,"line":3846},[137,78384,78233],{"class":4036},[137,78386,726],{"class":157},[137,78388,78389],{"class":284},"\"Perform a dry run without making changes\"\n",[137,78391,78392,78394,78396],{"class":139,"line":3861},[137,78393,78243],{"class":4036},[137,78395,726],{"class":157},[137,78397,78322],{"class":364},[137,78399,78400,78402,78404],{"class":139,"line":3883},[137,78401,78253],{"class":4036},[137,78403,726],{"class":157},[137,78405,78248],{"class":364},[137,78407,78408,78410,78412],{"class":139,"line":3896},[137,78409,78263],{"class":4036},[137,78411,726],{"class":157},[137,78413,78414],{"class":284},"boolean\n",[137,78416,78417,78420],{"class":139,"line":3901},[137,78418,78419],{"class":4036},"            version",[137,78421,36830],{"class":157},[137,78423,78424,78426,78428],{"class":139,"line":3906},[137,78425,78233],{"class":4036},[137,78427,726],{"class":157},[137,78429,78430],{"class":284},"\"Version to deploy (e.g., v1.0.0)\"\n",[137,78432,78433,78435,78437],{"class":139,"line":3911},[137,78434,78243],{"class":4036},[137,78436,726],{"class":157},[137,78438,78322],{"class":364},[137,78440,78441,78443,78445],{"class":139,"line":4666},[137,78442,78263],{"class":4036},[137,78444,726],{"class":157},[137,78446,78447],{"class":284},"string\n",[137,78449,78450],{"class":139,"line":4672},[137,78451,516],{"emptyLinePlaceholder":515},[137,78453,78454,78456],{"class":139,"line":4680},[137,78455,76304],{"class":4036},[137,78457,36830],{"class":157},[137,78459,78460,78463],{"class":139,"line":4711},[137,78461,78462],{"class":4036},"    deploy",[137,78464,36830],{"class":157},[137,78466,78467,78469,78471],{"class":139,"line":4716},[137,78468,76318],{"class":4036},[137,78470,726],{"class":157},[137,78472,76323],{"class":284},[137,78474,78475,78477],{"class":139,"line":4721},[137,78476,76328],{"class":4036},[137,78478,36830],{"class":157},[137,78480,78481,78483,78485,78487],{"class":139,"line":4727},[137,78482,76266],{"class":157},[137,78484,1387],{"class":4036},[137,78486,726],{"class":157},[137,78488,76341],{"class":284},[137,78490,78491,78493,78495],{"class":139,"line":4732},[137,78492,76346],{"class":4036},[137,78494,726],{"class":157},[137,78496,76351],{"class":284},[137,78498,78499],{"class":139,"line":5006},[137,78500,516],{"emptyLinePlaceholder":515},[137,78502,78503,78505,78507,78509],{"class":139,"line":5014},[137,78504,76266],{"class":157},[137,78506,1387],{"class":4036},[137,78508,726],{"class":157},[137,78510,78511],{"class":284},"Display inputs\n",[137,78513,78514,78516,78518],{"class":139,"line":14343},[137,78515,76371],{"class":4036},[137,78517,726],{"class":157},[137,78519,76448],{"class":143},[137,78521,78522],{"class":139,"line":24199},[137,78523,78524],{"class":284},"                  echo \"=== Workflow Dispatch Inputs ===\"\n",[137,78526,78527],{"class":139,"line":24773},[137,78528,78529],{"class":284},"                  echo \"Environment: ${{ inputs.environment }}\"\n",[137,78531,78532],{"class":139,"line":24778},[137,78533,78534],{"class":284},"                  echo \"Log Level: ${{ inputs.log_level }}\"\n",[137,78536,78537],{"class":139,"line":24783},[137,78538,78539],{"class":284},"                  echo \"Dry Run: ${{ inputs.dry_run }}\"\n",[137,78541,78542],{"class":139,"line":24793},[137,78543,78544],{"class":284},"                  echo \"Version: ${{ inputs.version || 'latest' }}\"\n",[137,78546,78547],{"class":139,"line":24827},[137,78548,78549],{"class":284},"                  echo \"================================\"\n",[137,78551,78552],{"class":139,"line":24857},[137,78553,516],{"emptyLinePlaceholder":515},[137,78555,78556,78558,78560,78562],{"class":139,"line":24862},[137,78557,76266],{"class":157},[137,78559,1387],{"class":4036},[137,78561,726],{"class":157},[137,78563,78564],{"class":284},"Simulate deployment\n",[137,78566,78567,78569,78571],{"class":139,"line":24867},[137,78568,76371],{"class":4036},[137,78570,726],{"class":157},[137,78572,76448],{"class":143},[137,78574,78575],{"class":139,"line":24884},[137,78576,78577],{"class":284},"                  echo \"Starting deployment process...\"\n",[137,78579,78580],{"class":139,"line":24892},[137,78581,78582],{"class":284},"                  echo \"Target environment: ${{ inputs.environment }}\"\n",[137,78584,78585],{"class":139,"line":24902},[137,78586,516],{"emptyLinePlaceholder":515},[137,78588,78589],{"class":139,"line":24925},[137,78590,78591],{"class":284},"                  if [ \"${{ inputs.dry_run }}\" = \"true\" ]; then\n",[137,78593,78594],{"class":139,"line":24933},[137,78595,78596],{"class":284},"                    echo \"[DRY RUN] Would deploy version ${{ inputs.version || 'latest' }} to ${{ inputs.environment }}\"\n",[137,78598,78599],{"class":139,"line":24941},[137,78600,78601],{"class":284},"                  else\n",[137,78603,78604],{"class":139,"line":24952},[137,78605,78606],{"class":284},"                    echo \"Deploying version ${{ inputs.version || 'latest' }} to ${{ inputs.environment }}\"\n",[137,78608,78609],{"class":139,"line":24960},[137,78610,78611],{"class":284},"                  fi\n",[137,78613,78614],{"class":139,"line":24982},[137,78615,516],{"emptyLinePlaceholder":515},[137,78617,78618],{"class":139,"line":24989},[137,78619,78620],{"class":284},"                  echo \"Deployment simulation complete!\"\n",[27,78622,78623],{},"To run this with custom inputs:",[128,78625,78627],{"className":8665,"code":78626,"language":8667,"meta":133,"style":133},"act workflow_dispatch -W .github\u002Fworkflows\u002Fworkflow-dispatch.yml \\\n  --input environment=production \\\n  --input log_level=debug \\\n  --input dry_run=false \\\n  --input version=v2.0.0\n",[22,78628,78629,78644,78654,78663,78674],{"__ignoreMap":133},[137,78630,78631,78633,78636,78638,78641],{"class":139,"line":140},[137,78632,75971],{"class":147},[137,78634,78635],{"class":284}," workflow_dispatch",[137,78637,76875],{"class":364},[137,78639,78640],{"class":284}," .github\u002Fworkflows\u002Fworkflow-dispatch.yml",[137,78642,78643],{"class":364}," \\\n",[137,78645,78646,78649,78652],{"class":139,"line":173},[137,78647,78648],{"class":364},"  --input",[137,78650,78651],{"class":284}," environment=production",[137,78653,78643],{"class":364},[137,78655,78656,78658,78661],{"class":139,"line":188},[137,78657,78648],{"class":364},[137,78659,78660],{"class":284}," log_level=debug",[137,78662,78643],{"class":364},[137,78664,78665,78667,78670,78672],{"class":139,"line":269},[137,78666,78648],{"class":364},[137,78668,78669],{"class":284}," dry_run=",[137,78671,30105],{"class":364},[137,78673,78643],{"class":364},[137,78675,78676,78678],{"class":139,"line":278},[137,78677,78648],{"class":364},[137,78679,78680],{"class":284}," version=v2.0.0\n",[27,78682,78683],{},"This is incredibly useful for testing deployment scripts before actually deploying.",[104,78685,78687],{"id":78686},"multi-job-workflows-with-dependencies","Multi-Job Workflows with Dependencies",[27,78689,78690],{},"Real-world CI\u002FCD pipelines often have multiple jobs that depend on each other. Let's create a workflow that demonstrates this pattern.",[27,78692,20297,78693,894],{},[22,78694,78695],{},".github\u002Fworkflows\u002Fmulti-job-dependencies.yml",[128,78697,78699],{"className":76224,"code":78698,"language":76226,"meta":133,"style":133},"name: 06 - Multi-Job with Dependencies\n\non:\n    push:\n        branches:\n            - main\n    workflow_dispatch:\n\njobs:\n    build:\n        runs-on: ubuntu-latest\n        outputs:\n            build_id: ${{ steps.build_step.outputs.build_id }}\n        steps:\n            - name: Checkout code\n              uses: actions\u002Fcheckout@v4\n\n            - name: Build step\n              id: build_step\n              run: |\n                  echo \"Building the project...\"\n                  BUILD_ID=\"build-$(date +%Y%m%d%H%M%S)\"\n                  echo \"build_id=$BUILD_ID\" >> $GITHUB_OUTPUT\n                  echo \"Build ID: $BUILD_ID\"\n                  echo \"Build completed successfully!\"\n\n    test:\n        needs: build\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout code\n              uses: actions\u002Fcheckout@v4\n\n            - name: Run tests\n              run: |\n                  echo \"Running tests for build: ${{ needs.build.outputs.build_id }}\"\n                  echo \"Test 1: Unit tests... PASSED\"\n                  echo \"Test 2: Integration tests... PASSED\"\n                  echo \"Test 3: E2E tests... PASSED\"\n                  echo \"All tests passed!\"\n\n    lint:\n        needs: build\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout code\n              uses: actions\u002Fcheckout@v4\n\n            - name: Run linting\n              run: |\n                  echo \"Running linting for build: ${{ needs.build.outputs.build_id }}\"\n                  echo \"Checking code style...\"\n                  echo \"Checking for common issues...\"\n                  echo \"Linting passed!\"\n\n    deploy:\n        needs: [test, lint]\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout code\n              uses: actions\u002Fcheckout@v4\n\n            - name: Deploy\n              run: |\n                  echo \"All checks passed!\"\n                  echo \"Deploying build: ${{ needs.build.outputs.build_id }}\"\n                  echo \"Deployment successful!\"\n",[22,78700,78701,78710,78714,78720,78726,78732,78738,78744,78748,78754,78761,78769,78776,78786,78792,78802,78810,78814,78825,78835,78843,78848,78853,78858,78863,78868,78872,78879,78889,78897,78903,78913,78921,78925,78936,78944,78949,78954,78959,78964,78969,78973,78980,78988,78996,79002,79012,79020,79024,79035,79043,79048,79053,79058,79063,79067,79073,79088,79096,79102,79112,79120,79124,79135,79143,79148,79153],{"__ignoreMap":133},[137,78702,78703,78705,78707],{"class":139,"line":140},[137,78704,1387],{"class":4036},[137,78706,726],{"class":157},[137,78708,78709],{"class":284},"06 - Multi-Job with Dependencies\n",[137,78711,78712],{"class":139,"line":173},[137,78713,516],{"emptyLinePlaceholder":515},[137,78715,78716,78718],{"class":139,"line":188},[137,78717,72117],{"class":364},[137,78719,36830],{"class":157},[137,78721,78722,78724],{"class":139,"line":269},[137,78723,76252],{"class":4036},[137,78725,36830],{"class":157},[137,78727,78728,78730],{"class":139,"line":278},[137,78729,76259],{"class":4036},[137,78731,36830],{"class":157},[137,78733,78734,78736],{"class":139,"line":291},[137,78735,76266],{"class":157},[137,78737,76269],{"class":284},[137,78739,78740,78742],{"class":139,"line":297},[137,78741,76678],{"class":4036},[137,78743,36830],{"class":157},[137,78745,78746],{"class":139,"line":302},[137,78747,516],{"emptyLinePlaceholder":515},[137,78749,78750,78752],{"class":139,"line":662},[137,78751,76304],{"class":4036},[137,78753,36830],{"class":157},[137,78755,78756,78759],{"class":139,"line":667},[137,78757,78758],{"class":4036},"    build",[137,78760,36830],{"class":157},[137,78762,78763,78765,78767],{"class":139,"line":786},[137,78764,76318],{"class":4036},[137,78766,726],{"class":157},[137,78768,76323],{"class":284},[137,78770,78771,78774],{"class":139,"line":798},[137,78772,78773],{"class":4036},"        outputs",[137,78775,36830],{"class":157},[137,78777,78778,78781,78783],{"class":139,"line":803},[137,78779,78780],{"class":4036},"            build_id",[137,78782,726],{"class":157},[137,78784,78785],{"class":284},"${{ steps.build_step.outputs.build_id }}\n",[137,78787,78788,78790],{"class":139,"line":931},[137,78789,76328],{"class":4036},[137,78791,36830],{"class":157},[137,78793,78794,78796,78798,78800],{"class":139,"line":1568},[137,78795,76266],{"class":157},[137,78797,1387],{"class":4036},[137,78799,726],{"class":157},[137,78801,76341],{"class":284},[137,78803,78804,78806,78808],{"class":139,"line":1573},[137,78805,76346],{"class":4036},[137,78807,726],{"class":157},[137,78809,76351],{"class":284},[137,78811,78812],{"class":139,"line":1578},[137,78813,516],{"emptyLinePlaceholder":515},[137,78815,78816,78818,78820,78822],{"class":139,"line":1588},[137,78817,76266],{"class":157},[137,78819,1387],{"class":4036},[137,78821,726],{"class":157},[137,78823,78824],{"class":284},"Build step\n",[137,78826,78827,78830,78832],{"class":139,"line":1601},[137,78828,78829],{"class":4036},"              id",[137,78831,726],{"class":157},[137,78833,78834],{"class":284},"build_step\n",[137,78836,78837,78839,78841],{"class":139,"line":3802},[137,78838,76371],{"class":4036},[137,78840,726],{"class":157},[137,78842,76448],{"class":143},[137,78844,78845],{"class":139,"line":3808},[137,78846,78847],{"class":284},"                  echo \"Building the project...\"\n",[137,78849,78850],{"class":139,"line":3822},[137,78851,78852],{"class":284},"                  BUILD_ID=\"build-$(date +%Y%m%d%H%M%S)\"\n",[137,78854,78855],{"class":139,"line":3827},[137,78856,78857],{"class":284},"                  echo \"build_id=$BUILD_ID\" >> $GITHUB_OUTPUT\n",[137,78859,78860],{"class":139,"line":3832},[137,78861,78862],{"class":284},"                  echo \"Build ID: $BUILD_ID\"\n",[137,78864,78865],{"class":139,"line":3840},[137,78866,78867],{"class":284},"                  echo \"Build completed successfully!\"\n",[137,78869,78870],{"class":139,"line":3846},[137,78871,516],{"emptyLinePlaceholder":515},[137,78873,78874,78877],{"class":139,"line":3861},[137,78875,78876],{"class":4036},"    test",[137,78878,36830],{"class":157},[137,78880,78881,78884,78886],{"class":139,"line":3883},[137,78882,78883],{"class":4036},"        needs",[137,78885,726],{"class":157},[137,78887,78888],{"class":284},"build\n",[137,78890,78891,78893,78895],{"class":139,"line":3896},[137,78892,76318],{"class":4036},[137,78894,726],{"class":157},[137,78896,76323],{"class":284},[137,78898,78899,78901],{"class":139,"line":3901},[137,78900,76328],{"class":4036},[137,78902,36830],{"class":157},[137,78904,78905,78907,78909,78911],{"class":139,"line":3906},[137,78906,76266],{"class":157},[137,78908,1387],{"class":4036},[137,78910,726],{"class":157},[137,78912,76341],{"class":284},[137,78914,78915,78917,78919],{"class":139,"line":3911},[137,78916,76346],{"class":4036},[137,78918,726],{"class":157},[137,78920,76351],{"class":284},[137,78922,78923],{"class":139,"line":4666},[137,78924,516],{"emptyLinePlaceholder":515},[137,78926,78927,78929,78931,78933],{"class":139,"line":4672},[137,78928,76266],{"class":157},[137,78930,1387],{"class":4036},[137,78932,726],{"class":157},[137,78934,78935],{"class":284},"Run tests\n",[137,78937,78938,78940,78942],{"class":139,"line":4680},[137,78939,76371],{"class":4036},[137,78941,726],{"class":157},[137,78943,76448],{"class":143},[137,78945,78946],{"class":139,"line":4711},[137,78947,78948],{"class":284},"                  echo \"Running tests for build: ${{ needs.build.outputs.build_id }}\"\n",[137,78950,78951],{"class":139,"line":4716},[137,78952,78953],{"class":284},"                  echo \"Test 1: Unit tests... PASSED\"\n",[137,78955,78956],{"class":139,"line":4721},[137,78957,78958],{"class":284},"                  echo \"Test 2: Integration tests... PASSED\"\n",[137,78960,78961],{"class":139,"line":4727},[137,78962,78963],{"class":284},"                  echo \"Test 3: E2E tests... PASSED\"\n",[137,78965,78966],{"class":139,"line":4732},[137,78967,78968],{"class":284},"                  echo \"All tests passed!\"\n",[137,78970,78971],{"class":139,"line":5006},[137,78972,516],{"emptyLinePlaceholder":515},[137,78974,78975,78978],{"class":139,"line":5014},[137,78976,78977],{"class":4036},"    lint",[137,78979,36830],{"class":157},[137,78981,78982,78984,78986],{"class":139,"line":14343},[137,78983,78883],{"class":4036},[137,78985,726],{"class":157},[137,78987,78888],{"class":284},[137,78989,78990,78992,78994],{"class":139,"line":24199},[137,78991,76318],{"class":4036},[137,78993,726],{"class":157},[137,78995,76323],{"class":284},[137,78997,78998,79000],{"class":139,"line":24773},[137,78999,76328],{"class":4036},[137,79001,36830],{"class":157},[137,79003,79004,79006,79008,79010],{"class":139,"line":24778},[137,79005,76266],{"class":157},[137,79007,1387],{"class":4036},[137,79009,726],{"class":157},[137,79011,76341],{"class":284},[137,79013,79014,79016,79018],{"class":139,"line":24783},[137,79015,76346],{"class":4036},[137,79017,726],{"class":157},[137,79019,76351],{"class":284},[137,79021,79022],{"class":139,"line":24793},[137,79023,516],{"emptyLinePlaceholder":515},[137,79025,79026,79028,79030,79032],{"class":139,"line":24827},[137,79027,76266],{"class":157},[137,79029,1387],{"class":4036},[137,79031,726],{"class":157},[137,79033,79034],{"class":284},"Run linting\n",[137,79036,79037,79039,79041],{"class":139,"line":24857},[137,79038,76371],{"class":4036},[137,79040,726],{"class":157},[137,79042,76448],{"class":143},[137,79044,79045],{"class":139,"line":24862},[137,79046,79047],{"class":284},"                  echo \"Running linting for build: ${{ needs.build.outputs.build_id }}\"\n",[137,79049,79050],{"class":139,"line":24867},[137,79051,79052],{"class":284},"                  echo \"Checking code style...\"\n",[137,79054,79055],{"class":139,"line":24884},[137,79056,79057],{"class":284},"                  echo \"Checking for common issues...\"\n",[137,79059,79060],{"class":139,"line":24892},[137,79061,79062],{"class":284},"                  echo \"Linting passed!\"\n",[137,79064,79065],{"class":139,"line":24902},[137,79066,516],{"emptyLinePlaceholder":515},[137,79068,79069,79071],{"class":139,"line":24925},[137,79070,78462],{"class":4036},[137,79072,36830],{"class":157},[137,79074,79075,79077,79079,79081,79083,79086],{"class":139,"line":24933},[137,79076,78883],{"class":4036},[137,79078,29669],{"class":157},[137,79080,53923],{"class":284},[137,79082,164],{"class":157},[137,79084,79085],{"class":284},"lint",[137,79087,33307],{"class":157},[137,79089,79090,79092,79094],{"class":139,"line":24941},[137,79091,76318],{"class":4036},[137,79093,726],{"class":157},[137,79095,76323],{"class":284},[137,79097,79098,79100],{"class":139,"line":24952},[137,79099,76328],{"class":4036},[137,79101,36830],{"class":157},[137,79103,79104,79106,79108,79110],{"class":139,"line":24960},[137,79105,76266],{"class":157},[137,79107,1387],{"class":4036},[137,79109,726],{"class":157},[137,79111,76341],{"class":284},[137,79113,79114,79116,79118],{"class":139,"line":24982},[137,79115,76346],{"class":4036},[137,79117,726],{"class":157},[137,79119,76351],{"class":284},[137,79121,79122],{"class":139,"line":24989},[137,79123,516],{"emptyLinePlaceholder":515},[137,79125,79126,79128,79130,79132],{"class":139,"line":24994},[137,79127,76266],{"class":157},[137,79129,1387],{"class":4036},[137,79131,726],{"class":157},[137,79133,79134],{"class":284},"Deploy\n",[137,79136,79137,79139,79141],{"class":139,"line":24999},[137,79138,76371],{"class":4036},[137,79140,726],{"class":157},[137,79142,76448],{"class":143},[137,79144,79145],{"class":139,"line":25004},[137,79146,79147],{"class":284},"                  echo \"All checks passed!\"\n",[137,79149,79150],{"class":139,"line":25032},[137,79151,79152],{"class":284},"                  echo \"Deploying build: ${{ needs.build.outputs.build_id }}\"\n",[137,79154,79155],{"class":139,"line":25040},[137,79156,79157],{"class":284},"                  echo \"Deployment successful!\"\n",[27,79159,79160],{},"Notice how:",[1003,79162,79163,79174,79185],{},[1006,79164,79165,114,79167,79169,79170,79173],{},[22,79166,53923],{},[22,79168,79085],{}," both depend on ",[22,79171,79172],{},"build"," (they run in parallel after build completes)",[1006,79175,79176,79179,79180,114,79182,79184],{},[22,79177,79178],{},"deploy"," depends on both ",[22,79181,53923],{},[22,79183,79085],{}," (it only runs after both pass)",[1006,79186,4737,79187,79190,79191],{},[22,79188,79189],{},"build_id"," output is passed between jobs using ",[22,79192,79193],{},"$GITHUB_OUTPUT",[27,79195,77378],{},[128,79197,79199],{"className":8665,"code":79198,"language":8667,"meta":133,"style":133},"act push -W .github\u002Fworkflows\u002Fmulti-job-dependencies.yml\n",[22,79200,79201],{"__ignoreMap":133},[137,79202,79203,79205,79207,79209],{"class":139,"line":140},[137,79204,75971],{"class":147},[137,79206,76582],{"class":284},[137,79208,76875],{"class":364},[137,79210,79211],{"class":284}," .github\u002Fworkflows\u002Fmulti-job-dependencies.yml\n",[104,79213,79215],{"id":79214},"matrix-builds","Matrix Builds",[27,79217,79218],{},"Matrix builds let you test across multiple configurations - different Node versions, operating systems, or any combination of parameters.",[27,79220,20297,79221,894],{},[22,79222,79223],{},".github\u002Fworkflows\u002Fmatrix-build.yml",[128,79225,79227],{"className":76224,"code":79226,"language":76226,"meta":133,"style":133},"name: 07 - Matrix Build Strategy\n\non:\n    push:\n        branches:\n            - main\n    workflow_dispatch:\n\njobs:\n    matrix-simple:\n        runs-on: ubuntu-latest\n        strategy:\n            matrix:\n                node: [18, 20, 22]\n        steps:\n            - name: Checkout code\n              uses: actions\u002Fcheckout@v4\n\n            - name: Setup Node.js ${{ matrix.node }}\n              uses: actions\u002Fsetup-node@v4\n              with:\n                  node-version: ${{ matrix.node }}\n\n            - name: Display versions\n              run: |\n                  echo \"Node version: $(node --version)\"\n                  echo \"npm version: $(npm --version)\"\n",[22,79228,79229,79238,79242,79248,79254,79260,79266,79272,79276,79282,79289,79297,79304,79311,79332,79338,79348,79356,79360,79371,79379,79385,79394,79398,79409,79417,79422],{"__ignoreMap":133},[137,79230,79231,79233,79235],{"class":139,"line":140},[137,79232,1387],{"class":4036},[137,79234,726],{"class":157},[137,79236,79237],{"class":284},"07 - Matrix Build Strategy\n",[137,79239,79240],{"class":139,"line":173},[137,79241,516],{"emptyLinePlaceholder":515},[137,79243,79244,79246],{"class":139,"line":188},[137,79245,72117],{"class":364},[137,79247,36830],{"class":157},[137,79249,79250,79252],{"class":139,"line":269},[137,79251,76252],{"class":4036},[137,79253,36830],{"class":157},[137,79255,79256,79258],{"class":139,"line":278},[137,79257,76259],{"class":4036},[137,79259,36830],{"class":157},[137,79261,79262,79264],{"class":139,"line":291},[137,79263,76266],{"class":157},[137,79265,76269],{"class":284},[137,79267,79268,79270],{"class":139,"line":297},[137,79269,76678],{"class":4036},[137,79271,36830],{"class":157},[137,79273,79274],{"class":139,"line":302},[137,79275,516],{"emptyLinePlaceholder":515},[137,79277,79278,79280],{"class":139,"line":662},[137,79279,76304],{"class":4036},[137,79281,36830],{"class":157},[137,79283,79284,79287],{"class":139,"line":667},[137,79285,79286],{"class":4036},"    matrix-simple",[137,79288,36830],{"class":157},[137,79290,79291,79293,79295],{"class":139,"line":786},[137,79292,76318],{"class":4036},[137,79294,726],{"class":157},[137,79296,76323],{"class":284},[137,79298,79299,79302],{"class":139,"line":798},[137,79300,79301],{"class":4036},"        strategy",[137,79303,36830],{"class":157},[137,79305,79306,79309],{"class":139,"line":803},[137,79307,79308],{"class":4036},"            matrix",[137,79310,36830],{"class":157},[137,79312,79313,79316,79318,79321,79323,79325,79327,79330],{"class":139,"line":931},[137,79314,79315],{"class":4036},"                node",[137,79317,29669],{"class":157},[137,79319,79320],{"class":364},"18",[137,79322,164],{"class":157},[137,79324,14727],{"class":364},[137,79326,164],{"class":157},[137,79328,79329],{"class":364},"22",[137,79331,33307],{"class":157},[137,79333,79334,79336],{"class":139,"line":1568},[137,79335,76328],{"class":4036},[137,79337,36830],{"class":157},[137,79339,79340,79342,79344,79346],{"class":139,"line":1573},[137,79341,76266],{"class":157},[137,79343,1387],{"class":4036},[137,79345,726],{"class":157},[137,79347,76341],{"class":284},[137,79349,79350,79352,79354],{"class":139,"line":1578},[137,79351,76346],{"class":4036},[137,79353,726],{"class":157},[137,79355,76351],{"class":284},[137,79357,79358],{"class":139,"line":1588},[137,79359,516],{"emptyLinePlaceholder":515},[137,79361,79362,79364,79366,79368],{"class":139,"line":1601},[137,79363,76266],{"class":157},[137,79365,1387],{"class":4036},[137,79367,726],{"class":157},[137,79369,79370],{"class":284},"Setup Node.js ${{ matrix.node }}\n",[137,79372,79373,79375,79377],{"class":139,"line":3802},[137,79374,76346],{"class":4036},[137,79376,726],{"class":157},[137,79378,78061],{"class":284},[137,79380,79381,79383],{"class":139,"line":3808},[137,79382,77202],{"class":4036},[137,79384,36830],{"class":157},[137,79386,79387,79389,79391],{"class":139,"line":3822},[137,79388,78072],{"class":4036},[137,79390,726],{"class":157},[137,79392,79393],{"class":284},"${{ matrix.node }}\n",[137,79395,79396],{"class":139,"line":3827},[137,79397,516],{"emptyLinePlaceholder":515},[137,79399,79400,79402,79404,79406],{"class":139,"line":3832},[137,79401,76266],{"class":157},[137,79403,1387],{"class":4036},[137,79405,726],{"class":157},[137,79407,79408],{"class":284},"Display versions\n",[137,79410,79411,79413,79415],{"class":139,"line":3840},[137,79412,76371],{"class":4036},[137,79414,726],{"class":157},[137,79416,76448],{"class":143},[137,79418,79419],{"class":139,"line":3846},[137,79420,79421],{"class":284},"                  echo \"Node version: $(node --version)\"\n",[137,79423,79424],{"class":139,"line":3861},[137,79425,79426],{"class":284},"                  echo \"npm version: $(npm --version)\"\n",[27,79428,79429],{},"To run a specific matrix combination:",[128,79431,79433],{"className":8665,"code":79432,"language":8667,"meta":133,"style":133},"act push -W .github\u002Fworkflows\u002Fmatrix-build.yml --matrix node:20\n",[22,79434,79435],{"__ignoreMap":133},[137,79436,79437,79439,79441,79443,79446,79449],{"class":139,"line":140},[137,79438,75971],{"class":147},[137,79440,76582],{"class":284},[137,79442,76875],{"class":364},[137,79444,79445],{"class":284}," .github\u002Fworkflows\u002Fmatrix-build.yml",[137,79447,79448],{"class":364}," --matrix",[137,79450,79451],{"class":284}," node:20\n",[104,79453,79455],{"id":79454},"working-with-secrets-and-environment-variables","Working with Secrets and Environment Variables",[27,79457,79458],{},"Workflows often need access to secrets - API keys, deployment tokens, and so on. Here's how to handle them with act.",[27,79460,20297,79461,894],{},[22,79462,79463],{},".github\u002Fworkflows\u002Fsecrets-and-env.yml",[128,79465,79467],{"className":76224,"code":79466,"language":76226,"meta":133,"style":133},"name: 08 - Secrets and Environment Variables\n\non:\n    push:\n        branches:\n            - main\n    workflow_dispatch:\n\nenv:\n    # Workflow-level environment variables\n    APP_NAME: \"my-awesome-app\"\n    APP_VERSION: \"1.0.0\"\n\njobs:\n    secrets-demo:\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout code\n              uses: actions\u002Fcheckout@v4\n\n            - name: Use secrets (masked in logs)\n              env:\n                  API_KEY: ${{ secrets.API_KEY }}\n                  DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}\n              run: |\n                  echo \"=== Working with Secrets ===\"\n                  echo \"Secrets are automatically masked in logs\"\n\n                  if [ -n \"$API_KEY\" ]; then\n                    echo \"API_KEY is set (value hidden)\"\n                  else\n                    echo \"API_KEY is not set - provide via 'act -s API_KEY=value'\"\n                  fi\n",[22,79468,79469,79478,79482,79488,79494,79500,79506,79512,79516,79523,79528,79538,79548,79552,79558,79565,79573,79579,79589,79597,79601,79612,79619,79629,79639,79647,79652,79657,79661,79666,79671,79675,79680],{"__ignoreMap":133},[137,79470,79471,79473,79475],{"class":139,"line":140},[137,79472,1387],{"class":4036},[137,79474,726],{"class":157},[137,79476,79477],{"class":284},"08 - Secrets and Environment Variables\n",[137,79479,79480],{"class":139,"line":173},[137,79481,516],{"emptyLinePlaceholder":515},[137,79483,79484,79486],{"class":139,"line":188},[137,79485,72117],{"class":364},[137,79487,36830],{"class":157},[137,79489,79490,79492],{"class":139,"line":269},[137,79491,76252],{"class":4036},[137,79493,36830],{"class":157},[137,79495,79496,79498],{"class":139,"line":278},[137,79497,76259],{"class":4036},[137,79499,36830],{"class":157},[137,79501,79502,79504],{"class":139,"line":291},[137,79503,76266],{"class":157},[137,79505,76269],{"class":284},[137,79507,79508,79510],{"class":139,"line":297},[137,79509,76678],{"class":4036},[137,79511,36830],{"class":157},[137,79513,79514],{"class":139,"line":302},[137,79515,516],{"emptyLinePlaceholder":515},[137,79517,79518,79521],{"class":139,"line":662},[137,79519,79520],{"class":4036},"env",[137,79522,36830],{"class":157},[137,79524,79525],{"class":139,"line":667},[137,79526,79527],{"class":308},"    # Workflow-level environment variables\n",[137,79529,79530,79533,79535],{"class":139,"line":786},[137,79531,79532],{"class":4036},"    APP_NAME",[137,79534,726],{"class":157},[137,79536,79537],{"class":284},"\"my-awesome-app\"\n",[137,79539,79540,79543,79545],{"class":139,"line":798},[137,79541,79542],{"class":4036},"    APP_VERSION",[137,79544,726],{"class":157},[137,79546,79547],{"class":284},"\"1.0.0\"\n",[137,79549,79550],{"class":139,"line":803},[137,79551,516],{"emptyLinePlaceholder":515},[137,79553,79554,79556],{"class":139,"line":931},[137,79555,76304],{"class":4036},[137,79557,36830],{"class":157},[137,79559,79560,79563],{"class":139,"line":1568},[137,79561,79562],{"class":4036},"    secrets-demo",[137,79564,36830],{"class":157},[137,79566,79567,79569,79571],{"class":139,"line":1573},[137,79568,76318],{"class":4036},[137,79570,726],{"class":157},[137,79572,76323],{"class":284},[137,79574,79575,79577],{"class":139,"line":1578},[137,79576,76328],{"class":4036},[137,79578,36830],{"class":157},[137,79580,79581,79583,79585,79587],{"class":139,"line":1588},[137,79582,76266],{"class":157},[137,79584,1387],{"class":4036},[137,79586,726],{"class":157},[137,79588,76341],{"class":284},[137,79590,79591,79593,79595],{"class":139,"line":1601},[137,79592,76346],{"class":4036},[137,79594,726],{"class":157},[137,79596,76351],{"class":284},[137,79598,79599],{"class":139,"line":3802},[137,79600,516],{"emptyLinePlaceholder":515},[137,79602,79603,79605,79607,79609],{"class":139,"line":3808},[137,79604,76266],{"class":157},[137,79606,1387],{"class":4036},[137,79608,726],{"class":157},[137,79610,79611],{"class":284},"Use secrets (masked in logs)\n",[137,79613,79614,79617],{"class":139,"line":3822},[137,79615,79616],{"class":4036},"              env",[137,79618,36830],{"class":157},[137,79620,79621,79624,79626],{"class":139,"line":3827},[137,79622,79623],{"class":4036},"                  API_KEY",[137,79625,726],{"class":157},[137,79627,79628],{"class":284},"${{ secrets.API_KEY }}\n",[137,79630,79631,79634,79636],{"class":139,"line":3832},[137,79632,79633],{"class":4036},"                  DATABASE_PASSWORD",[137,79635,726],{"class":157},[137,79637,79638],{"class":284},"${{ secrets.DATABASE_PASSWORD }}\n",[137,79640,79641,79643,79645],{"class":139,"line":3840},[137,79642,76371],{"class":4036},[137,79644,726],{"class":157},[137,79646,76448],{"class":143},[137,79648,79649],{"class":139,"line":3846},[137,79650,79651],{"class":284},"                  echo \"=== Working with Secrets ===\"\n",[137,79653,79654],{"class":139,"line":3861},[137,79655,79656],{"class":284},"                  echo \"Secrets are automatically masked in logs\"\n",[137,79658,79659],{"class":139,"line":3883},[137,79660,516],{"emptyLinePlaceholder":515},[137,79662,79663],{"class":139,"line":3896},[137,79664,79665],{"class":284},"                  if [ -n \"$API_KEY\" ]; then\n",[137,79667,79668],{"class":139,"line":3901},[137,79669,79670],{"class":284},"                    echo \"API_KEY is set (value hidden)\"\n",[137,79672,79673],{"class":139,"line":3906},[137,79674,78601],{"class":284},[137,79676,79677],{"class":139,"line":3911},[137,79678,79679],{"class":284},"                    echo \"API_KEY is not set - provide via 'act -s API_KEY=value'\"\n",[137,79681,79682],{"class":139,"line":4666},[137,79683,78611],{"class":284},[27,79685,79686],{},"To pass secrets to act:",[128,79688,79690],{"className":8665,"code":79689,"language":8667,"meta":133,"style":133},"# Inline secrets\nact push -s API_KEY=my-api-key -s DATABASE_PASSWORD=my-password\n\n# From a file (create .secrets with KEY=value pairs)\nact push --secret-file .secrets\n\n# Using GitHub CLI token\nact push -s GITHUB_TOKEN=\"$(gh auth token)\"\n",[22,79691,79692,79697,79714,79718,79723,79735,79739,79744],{"__ignoreMap":133},[137,79693,79694],{"class":139,"line":140},[137,79695,79696],{"class":308},"# Inline secrets\n",[137,79698,79699,79701,79703,79706,79709,79711],{"class":139,"line":173},[137,79700,75971],{"class":147},[137,79702,76582],{"class":284},[137,79704,79705],{"class":364}," -s",[137,79707,79708],{"class":284}," API_KEY=my-api-key",[137,79710,79705],{"class":364},[137,79712,79713],{"class":284}," DATABASE_PASSWORD=my-password\n",[137,79715,79716],{"class":139,"line":188},[137,79717,516],{"emptyLinePlaceholder":515},[137,79719,79720],{"class":139,"line":269},[137,79721,79722],{"class":308},"# From a file (create .secrets with KEY=value pairs)\n",[137,79724,79725,79727,79729,79732],{"class":139,"line":278},[137,79726,75971],{"class":147},[137,79728,76582],{"class":284},[137,79730,79731],{"class":364}," --secret-file",[137,79733,79734],{"class":284}," .secrets\n",[137,79736,79737],{"class":139,"line":291},[137,79738,516],{"emptyLinePlaceholder":515},[137,79740,79741],{"class":139,"line":297},[137,79742,79743],{"class":308},"# Using GitHub CLI token\n",[137,79745,79746,79748,79750,79752,79755,79758],{"class":139,"line":302},[137,79747,75971],{"class":147},[137,79749,76582],{"class":284},[137,79751,79705],{"class":364},[137,79753,79754],{"class":284}," GITHUB_TOKEN=\"$(",[137,79756,79757],{"class":147},"gh",[137,79759,79760],{"class":284}," auth token)\"\n",[3244,79762,79763],{},[27,79764,79765,79766,79769,79770,1017],{},"Never commit your ",[22,79767,79768],{},".secrets"," file! Add it to ",[22,79771,61421],{},[104,79773,79775],{"id":79774},"caching-dependencies","Caching Dependencies",[27,79777,79778],{},"Caching can significantly speed up your workflows. Here's how to set it up.",[27,79780,79781,79782,114,79784,79787],{},"First, make sure you have ",[22,79783,5140],{},[22,79785,79786],{},"requirements.txt"," in your project root:",[27,79789,79790],{},[42,79791,79792],{},"package.json:",[128,79794,79796],{"className":5155,"code":79795,"language":5157,"meta":133,"style":133},"{\n    \"name\": \"act-demo\",\n    \"version\": \"1.0.0\",\n    \"dependencies\": {\n        \"lodash\": \"^4.17.21\"\n    }\n}\n",[22,79797,79798,79802,79814,79826,79833,79843,79847],{"__ignoreMap":133},[137,79799,79800],{"class":139,"line":140},[137,79801,15971],{"class":157},[137,79803,79804,79807,79809,79812],{"class":139,"line":173},[137,79805,79806],{"class":364},"    \"name\"",[137,79808,726],{"class":157},[137,79810,79811],{"class":284},"\"act-demo\"",[137,79813,1961],{"class":157},[137,79815,79816,79819,79821,79824],{"class":139,"line":188},[137,79817,79818],{"class":364},"    \"version\"",[137,79820,726],{"class":157},[137,79822,79823],{"class":284},"\"1.0.0\"",[137,79825,1961],{"class":157},[137,79827,79828,79831],{"class":139,"line":269},[137,79829,79830],{"class":364},"    \"dependencies\"",[137,79832,1819],{"class":157},[137,79834,79835,79838,79840],{"class":139,"line":278},[137,79836,79837],{"class":364},"        \"lodash\"",[137,79839,726],{"class":157},[137,79841,79842],{"class":284},"\"^4.17.21\"\n",[137,79844,79845],{"class":139,"line":291},[137,79846,294],{"class":157},[137,79848,79849],{"class":139,"line":297},[137,79850,510],{"class":157},[27,79852,79853],{},[42,79854,79855],{},"requirements.txt:",[128,79857,79860],{"className":79858,"code":79859,"language":5189},[5187],"requests==2.31.0\n",[22,79861,79859],{"__ignoreMap":133},[27,79863,79864],{},"Generate the lock file:",[128,79866,79868],{"className":8665,"code":79867,"language":8667,"meta":133,"style":133},"npm install --package-lock-only\n",[22,79869,79870],{"__ignoreMap":133},[137,79871,79872,79874,79876],{"class":139,"line":140},[137,79873,9536],{"class":147},[137,79875,10268],{"class":284},[137,79877,79878],{"class":364}," --package-lock-only\n",[27,79880,79881,79882,894],{},"Now create ",[22,79883,79884],{},".github\u002Fworkflows\u002Fcaching.yml",[128,79886,79888],{"className":76224,"code":79887,"language":76226,"meta":133,"style":133},"name: 11 - Caching Dependencies\n\non:\n    push:\n        branches:\n            - main\n    workflow_dispatch:\n\njobs:\n    cache-npm:\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout code\n              uses: actions\u002Fcheckout@v4\n\n            - name: Setup Node.js with cache\n              uses: actions\u002Fsetup-node@v4\n              with:\n                  node-version: \"20\"\n                  cache: \"npm\"\n\n            - name: Install dependencies\n              run: npm install\n\n            - name: Verify installation\n              run: |\n                  echo \"Node modules installed:\"\n                  ls node_modules\u002F\n\n    cache-pip:\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout code\n              uses: actions\u002Fcheckout@v4\n\n            - name: Setup Python with cache\n              uses: actions\u002Fsetup-python@v5\n              with:\n                  python-version: \"3.12\"\n                  cache: \"pip\"\n\n            - name: Install dependencies\n              run: pip install -r requirements.txt\n\n            - name: Verify installation\n              run: |\n                  python -c \"import requests; print(f'requests version: {requests.__version__}')\"\n",[22,79889,79890,79899,79903,79909,79915,79921,79927,79933,79937,79943,79950,79958,79964,79974,79982,79986,79997,80005,80011,80019,80029,80033,80044,80053,80057,80068,80076,80081,80086,80090,80097,80105,80111,80121,80129,80133,80144,80152,80158,80166,80175,80179,80189,80198,80202,80212,80220],{"__ignoreMap":133},[137,79891,79892,79894,79896],{"class":139,"line":140},[137,79893,1387],{"class":4036},[137,79895,726],{"class":157},[137,79897,79898],{"class":284},"11 - Caching Dependencies\n",[137,79900,79901],{"class":139,"line":173},[137,79902,516],{"emptyLinePlaceholder":515},[137,79904,79905,79907],{"class":139,"line":188},[137,79906,72117],{"class":364},[137,79908,36830],{"class":157},[137,79910,79911,79913],{"class":139,"line":269},[137,79912,76252],{"class":4036},[137,79914,36830],{"class":157},[137,79916,79917,79919],{"class":139,"line":278},[137,79918,76259],{"class":4036},[137,79920,36830],{"class":157},[137,79922,79923,79925],{"class":139,"line":291},[137,79924,76266],{"class":157},[137,79926,76269],{"class":284},[137,79928,79929,79931],{"class":139,"line":297},[137,79930,76678],{"class":4036},[137,79932,36830],{"class":157},[137,79934,79935],{"class":139,"line":302},[137,79936,516],{"emptyLinePlaceholder":515},[137,79938,79939,79941],{"class":139,"line":662},[137,79940,76304],{"class":4036},[137,79942,36830],{"class":157},[137,79944,79945,79948],{"class":139,"line":667},[137,79946,79947],{"class":4036},"    cache-npm",[137,79949,36830],{"class":157},[137,79951,79952,79954,79956],{"class":139,"line":786},[137,79953,76318],{"class":4036},[137,79955,726],{"class":157},[137,79957,76323],{"class":284},[137,79959,79960,79962],{"class":139,"line":798},[137,79961,76328],{"class":4036},[137,79963,36830],{"class":157},[137,79965,79966,79968,79970,79972],{"class":139,"line":803},[137,79967,76266],{"class":157},[137,79969,1387],{"class":4036},[137,79971,726],{"class":157},[137,79973,76341],{"class":284},[137,79975,79976,79978,79980],{"class":139,"line":931},[137,79977,76346],{"class":4036},[137,79979,726],{"class":157},[137,79981,76351],{"class":284},[137,79983,79984],{"class":139,"line":1568},[137,79985,516],{"emptyLinePlaceholder":515},[137,79987,79988,79990,79992,79994],{"class":139,"line":1573},[137,79989,76266],{"class":157},[137,79991,1387],{"class":4036},[137,79993,726],{"class":157},[137,79995,79996],{"class":284},"Setup Node.js with cache\n",[137,79998,79999,80001,80003],{"class":139,"line":1578},[137,80000,76346],{"class":4036},[137,80002,726],{"class":157},[137,80004,78061],{"class":284},[137,80006,80007,80009],{"class":139,"line":1588},[137,80008,77202],{"class":4036},[137,80010,36830],{"class":157},[137,80012,80013,80015,80017],{"class":139,"line":1601},[137,80014,78072],{"class":4036},[137,80016,726],{"class":157},[137,80018,78077],{"class":284},[137,80020,80021,80024,80026],{"class":139,"line":3802},[137,80022,80023],{"class":4036},"                  cache",[137,80025,726],{"class":157},[137,80027,80028],{"class":284},"\"npm\"\n",[137,80030,80031],{"class":139,"line":3808},[137,80032,516],{"emptyLinePlaceholder":515},[137,80034,80035,80037,80039,80041],{"class":139,"line":3822},[137,80036,76266],{"class":157},[137,80038,1387],{"class":4036},[137,80040,726],{"class":157},[137,80042,80043],{"class":284},"Install dependencies\n",[137,80045,80046,80048,80050],{"class":139,"line":3827},[137,80047,76371],{"class":4036},[137,80049,726],{"class":157},[137,80051,80052],{"class":284},"npm install\n",[137,80054,80055],{"class":139,"line":3832},[137,80056,516],{"emptyLinePlaceholder":515},[137,80058,80059,80061,80063,80065],{"class":139,"line":3840},[137,80060,76266],{"class":157},[137,80062,1387],{"class":4036},[137,80064,726],{"class":157},[137,80066,80067],{"class":284},"Verify installation\n",[137,80069,80070,80072,80074],{"class":139,"line":3846},[137,80071,76371],{"class":4036},[137,80073,726],{"class":157},[137,80075,76448],{"class":143},[137,80077,80078],{"class":139,"line":3861},[137,80079,80080],{"class":284},"                  echo \"Node modules installed:\"\n",[137,80082,80083],{"class":139,"line":3883},[137,80084,80085],{"class":284},"                  ls node_modules\u002F\n",[137,80087,80088],{"class":139,"line":3896},[137,80089,516],{"emptyLinePlaceholder":515},[137,80091,80092,80095],{"class":139,"line":3901},[137,80093,80094],{"class":4036},"    cache-pip",[137,80096,36830],{"class":157},[137,80098,80099,80101,80103],{"class":139,"line":3906},[137,80100,76318],{"class":4036},[137,80102,726],{"class":157},[137,80104,76323],{"class":284},[137,80106,80107,80109],{"class":139,"line":3911},[137,80108,76328],{"class":4036},[137,80110,36830],{"class":157},[137,80112,80113,80115,80117,80119],{"class":139,"line":4666},[137,80114,76266],{"class":157},[137,80116,1387],{"class":4036},[137,80118,726],{"class":157},[137,80120,76341],{"class":284},[137,80122,80123,80125,80127],{"class":139,"line":4672},[137,80124,76346],{"class":4036},[137,80126,726],{"class":157},[137,80128,76351],{"class":284},[137,80130,80131],{"class":139,"line":4680},[137,80132,516],{"emptyLinePlaceholder":515},[137,80134,80135,80137,80139,80141],{"class":139,"line":4711},[137,80136,76266],{"class":157},[137,80138,1387],{"class":4036},[137,80140,726],{"class":157},[137,80142,80143],{"class":284},"Setup Python with cache\n",[137,80145,80146,80148,80150],{"class":139,"line":4716},[137,80147,76346],{"class":4036},[137,80149,726],{"class":157},[137,80151,77197],{"class":284},[137,80153,80154,80156],{"class":139,"line":4721},[137,80155,77202],{"class":4036},[137,80157,36830],{"class":157},[137,80159,80160,80162,80164],{"class":139,"line":4727},[137,80161,77209],{"class":4036},[137,80163,726],{"class":157},[137,80165,77214],{"class":284},[137,80167,80168,80170,80172],{"class":139,"line":4732},[137,80169,80023],{"class":4036},[137,80171,726],{"class":157},[137,80173,80174],{"class":284},"\"pip\"\n",[137,80176,80177],{"class":139,"line":5006},[137,80178,516],{"emptyLinePlaceholder":515},[137,80180,80181,80183,80185,80187],{"class":139,"line":5014},[137,80182,76266],{"class":157},[137,80184,1387],{"class":4036},[137,80186,726],{"class":157},[137,80188,80043],{"class":284},[137,80190,80191,80193,80195],{"class":139,"line":14343},[137,80192,76371],{"class":4036},[137,80194,726],{"class":157},[137,80196,80197],{"class":284},"pip install -r requirements.txt\n",[137,80199,80200],{"class":139,"line":24199},[137,80201,516],{"emptyLinePlaceholder":515},[137,80203,80204,80206,80208,80210],{"class":139,"line":24773},[137,80205,76266],{"class":157},[137,80207,1387],{"class":4036},[137,80209,726],{"class":157},[137,80211,80067],{"class":284},[137,80213,80214,80216,80218],{"class":139,"line":24778},[137,80215,76371],{"class":4036},[137,80217,726],{"class":157},[137,80219,76448],{"class":143},[137,80221,80222],{"class":139,"line":24783},[137,80223,80224],{"class":284},"                  python -c \"import requests; print(f'requests version: {requests.__version__}')\"\n",[27,80226,77378],{},[128,80228,80230],{"className":8665,"code":80229,"language":8667,"meta":133,"style":133},"act push -W .github\u002Fworkflows\u002Fcaching.yml\n",[22,80231,80232],{"__ignoreMap":133},[137,80233,80234,80236,80238,80240],{"class":139,"line":140},[137,80235,75971],{"class":147},[137,80237,76582],{"class":284},[137,80239,76875],{"class":364},[137,80241,80242],{"class":284}," .github\u002Fworkflows\u002Fcaching.yml\n",[27,80244,80245],{},"The first run will install dependencies. Subsequent runs will restore them from cache - much faster!",[104,80247,80249],{"id":80248},"the-vscode-extension","The VSCode Extension",[27,80251,80252,80253,80260],{},"By now, we've been running GitHub Actions locally through the command line - and for many developers, that's perfectly fine. But if you prefer a more visual approach, there's a fantastic VSCode extension called ",[45,80254,80257],{"href":80255,"target":2716,"rel":80256},"https:\u002F\u002Fsanjulaganepola.github.io\u002Fgithub-local-actions-docs\u002F",[2718,2719],[42,80258,80259],{},"GitHub Local Actions"," that lets you run workflows with the click of a button.",[27,80262,80263,80264,80266],{},"The extension is built on top of the same ",[42,80265,75971],{}," CLI we've been using, so everything we've learned still applies. It just wraps it in a friendly interface that integrates directly into your editor.",[123,80268,80270],{"id":80269},"running-a-workflow","Running a Workflow",[27,80272,80273],{},"With the extension, running a workflow is as simple as:",[2569,80275,80276,80283,80286],{},[1006,80277,80278,80279,80282],{},"Open the ",[42,80280,80281],{},"Workflows"," view in the sidebar",[1006,80284,80285],{},"Find your workflow (e.g., \"Simple Push Action\")",[1006,80287,80288,80289,80292],{},"Click the ",[42,80290,80291],{},"play button"," next to it",[27,80294,80295,80296,80298],{},"The extension builds the appropriate ",[42,80297,75971],{}," command behind the scenes and runs it as a VSCode task. You'll see the output in the integrated terminal, complete with the same colourful logs you'd see from the CLI.",[27,80300,80301],{},"You can also right-click on a specific job within a workflow to run just that job - useful when you're debugging one part of a multi-job pipeline.",[123,80303,80305],{"id":80304},"managing-secrets-and-variables","Managing Secrets and Variables",[27,80307,80308,80309,80312],{},"Remember how we passed secrets via ",[22,80310,80311],{},"-s API_KEY=value"," on the command line? The extension makes this easier:",[2569,80314,80315,80321,80327],{},[1006,80316,80278,80317,80320],{},[42,80318,80319],{},"Settings"," view",[1006,80322,80323,80324],{},"Navigate to ",[42,80325,80326],{},"Secrets",[1006,80328,80329],{},"Add your secrets with their values",[27,80331,80332],{},"They're stored securely and automatically passed to your workflow runs. The same goes for variables, inputs, and runner configurations.",[104,80334,59075],{"id":59074},[27,80336,80337],{},"This is a tool I wish I'd known about much earlier - it would have saved me a lot of time. It's simple but powerful, and for workflows that need testing, it can dramatically reduce iteration time.",[27,80339,80340],{},"I've documented this for myself so I can come back sometime in the future when I need to test my workflows and remind myself from this article.",[27,80342,80343],{},"I've shown a few examples here, but there are even more in the following repo. You can find all these examples and additional ones here:",[27,80345,80346,726,80349],{},[42,80347,80348],{},"Repository",[45,80350,80353],{"href":80351,"target":2716,"rel":80352},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Frun-your-github-actions-locally-with-act",[2718,2719],"run-your-github-actions-locally-with-act",[27,80355,80356],{},"Happy testing!",[2617,80358,80359],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":133,"searchDepth":173,"depth":173,"links":80361},[80362,80363,80367,80368,80369,80370,80371,80372,80373,80374,80375,80376,80377,80381],{"id":75997,"depth":173,"text":75998},{"id":66828,"depth":173,"text":66831,"children":80364},[80365,80366],{"id":76101,"depth":188,"text":76102},{"id":76164,"depth":188,"text":76165},{"id":76214,"depth":173,"text":76215},{"id":76542,"depth":173,"text":76543},{"id":76594,"depth":173,"text":76595},{"id":76887,"depth":173,"text":76888},{"id":77409,"depth":173,"text":77410},{"id":78175,"depth":173,"text":78176},{"id":78686,"depth":173,"text":78687},{"id":79214,"depth":173,"text":79215},{"id":79454,"depth":173,"text":79455},{"id":79774,"depth":173,"text":79775},{"id":80248,"depth":173,"text":80249,"children":80378},[80379,80380],{"id":80269,"depth":188,"text":80270},{"id":80304,"depth":188,"text":80305},{"id":59074,"depth":173,"text":59075},"Learn how to run and debug GitHub Actions workflows locally using act. This comprehensive guide covers installation, running workflows, handling secrets, matrix builds, caching, and multi-job pipelines. Stop the commit-push-wait-fail cycle and catch issues before they reach your repository.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1769425117\u002Fblog\u002Fhow-to-run-and-debug-your-github-workflows-locally\u002Fhow-to-run-and-debug-your-github-workflows-locally_afbogh",[80385,80386,80387,80388,80389,80390,80391,80392,80393,80394,80395,80396,80397,80398,80399,80400,80401,80402,80259,80403],"GitHub Actions","act CLI","local workflow testing","CI\u002FCD debugging","GitHub Actions locally","Docker containers","workflow automation","cron jobs","matrix builds","secrets management","environment variables","TypeScript workflows","Python workflows","multi-job pipelines","GitHub runner","workflow dispatch","dependency caching","VSCode extension","Apple Silicon",{},"\u002F2026\u002F02\u002F01\u002Fhow-to-run-and-debug-your-github-workflows-locally","1st February 2026",{"title":75903,"description":80382},"2026\u002F02\u002F01\u002Fhow-to-run-and-debug-your-github-workflows-locally","ITlR5Dz6M2MVjy9-epPjiT0Rc358V-7oranW8mVMWoc",{"id":80411,"title":80412,"articleTags":80413,"author":11,"blog":12,"body":80414,"description":82400,"extension":2649,"image":82401,"keywords":82402,"meta":82419,"navigation":515,"path":82420,"published":82421,"readTime":662,"seo":82422,"stem":82423,"type":2662,"__hash__":82424},"content\u002F2026\u002F02\u002F08\u002Fautomating-my-blog-workflow-with-github-copilot-sdk.md","Automating My Blog Workflow with GitHub Copilot SDK",[12817,2669,27886],{"type":14,"value":80415,"toc":82383},[80416,80419,80433,80435,80439,80444,80447,80454,80458,80461,80494,80497,80501,80504,80507,80510,80514,80522,80525,80546,80549,80575,80584,80588,80593,80604,80607,80613,80616,80648,80654,80658,80661,80663,80672,80776,80789,80793,80796,80852,80855,80859,80862,80987,81002,81017,81020,81024,81027,81335,81338,81355,81362,81366,81369,81848,81851,81856,81885,81888,81893,81926,81937,81942,81969,81974,81979,81994,82000,82005,82043,82050,82054,82057,82295,82298,82302,82305,82326,82329,82335,82338,82342,82345,82348,82352,82355,82358,82361,82377,82380],[17,80417,80412],{"id":80418},"automating-my-blog-workflow-with-github-copilot-sdk",[27,80420,80421],{},[30,80422,80423,36,80425,40,80427],{},[33,80424],{"value":35},[33,80426],{"value":39},[42,80428,80429],{},[45,80430,80431],{"href":47},[33,80432],{"value":50},[52,80434],{":tags":54},[56,80436],{":audio-src":80437,":transcript-src":80438},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F02\u002F08\u002Fautomating-my-blog-workflow-with-github-copilot-sdk\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F02\u002F08\u002Fautomating-my-blog-workflow-with-github-copilot-sdk\u002Fsummary.json",[27,80440,80441],{},[63,80442],{"alt":12847,"src":80443},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1770033517\u002Fblog\u002Fautomating-my-blog-workflow-with-github-copilot-sdk\u002Fautomating-my-blog-workflow-with-github-copilot-sdk_atnuqy",[27,80445,80446],{},"When GitHub announced the Copilot SDK in technical preview a few weeks back in January, my mind immediately started racing with possibilities. I've been writing blog articles for half a decade now, and while I love the creative process of putting thoughts into words, there's always been a part of the workflow that felt... tedious.",[27,80448,80449,80450,80453],{},"I write all my blog posts in Markdown. It's clean, it's developer-friendly, and it works beautifully with note-taking apps like ",[45,80451,71794],{"href":71792,"target":2716,"rel":80452},[2718,2719],". But turning a raw Markdown draft into a properly formatted blog article? That's where things used to get repetitive.",[104,80455,80457],{"id":80456},"the-manual-work-i-was-doing","The Manual Work I Was Doing",[27,80459,80460],{},"Every time I finished writing an article, I had to go through a checklist of tasks:",[1003,80462,80463,80469,80472,80475,80478,80481,80488,80491],{},[1006,80464,80465,80466,14105],{},"Convert the draft file into the correct folder structure (",[22,80467,80468],{},"content\u002FYYYY\u002FMM\u002FDD\u002Farticle-slug.md",[1006,80470,80471],{},"Add frontmatter with SEO metadata (title, description, keywords)",[1006,80473,80474],{},"Calculate and add the reading time",[1006,80476,80477],{},"Format the publication date in a human-readable way (\"2nd February 2026\")",[1006,80479,80480],{},"Add article tags (my blog uses exactly 3 tags per article)",[1006,80482,80483,80484,80487],{},"Apply the correct ",[45,80485,61587],{"href":61585,"target":2716,"rel":80486},[2718,2719]," image modifiers for cover images and inline images",[1006,80489,80490],{},"Make sure external links open in new tabs with proper security attributes",[1006,80492,80493],{},"Add the required components in the right places",[27,80495,80496],{},"It took time. And when you're excited to publish something you've just written, spending more time on formatting feels like a hassle.",[104,80498,80500],{"id":80499},"enter-github-copilot","Enter GitHub Copilot",[27,80502,80503],{},"When agents were first introduced in Copilot last year, I started using GitHub Copilot in VS Code to help with this process. I'd open the Copilot chat, write a detailed prompt explaining exactly what I needed, and let it do the heavy lifting. It worked well - Copilot understood my blog's structure after I showed it a few examples, and it could generate the formatted article reliably.",[27,80505,80506],{},"But here's the thing: I was still writing the same prompt over and over. Copy the draft content, paste it into Copilot, include references to existing articles for context, specify the tags, the date format, the image URLs... It was faster than doing everything manually, but I kept thinking: \"Surely I can automate this too?\"",[27,80508,80509],{},"That's when the Copilot SDK caught my attention.",[104,80511,80513],{"id":80512},"what-is-the-github-copilot-sdk","What is the GitHub Copilot SDK?",[27,80515,4737,80516,80521],{},[45,80517,80520],{"href":80518,"target":2716,"rel":80519},"https:\u002F\u002Fgithub.com\u002Fgithub\u002Fcopilot-sdk",[2718,2719],"GitHub Copilot SDK"," gives developers programmatic access to the same agentic engine that powers the GitHub Copilot CLI. Instead of interacting with Copilot through a chat interface, you can write code that talks to it directly.",[27,80523,80524],{},"The SDK is available in four languages:",[1003,80526,80527,80532,80536,80541],{},[1006,80528,80529],{},[42,80530,80531],{},"Node.js\u002FTypeScript",[1006,80533,80534],{},[42,80535,28614],{},[1006,80537,80538],{},[42,80539,80540],{},"Go",[1006,80542,80543],{},[42,80544,80545],{},".NET",[27,80547,80548],{},"They all provide a consistent API with features like:",[1003,80550,80551,80557,80563,80569],{},[1006,80552,80553,80556],{},[42,80554,80555],{},"Multi-turn conversations"," - Sessions maintain context across interactions",[1006,80558,80559,80562],{},[42,80560,80561],{},"File attachments"," - Attach files for the model to analyse",[1006,80564,80565,80568],{},[42,80566,80567],{},"Tool execution"," - Define custom tools the model can invoke",[1006,80570,80571,80574],{},[42,80572,80573],{},"Model selection"," - Choose which model to use for your task",[3244,80576,80577],{},[27,80578,80579,80580,80583],{},"This is still in ",[42,80581,80582],{},"technical preview",". The documentation might change, features could evolve, and it's not yet recommended for production use. But for personal projects and experimentation? It's already quite capable.",[104,80585,80587],{"id":80586},"how-the-sdk-works","How the SDK Works",[27,80589,80590],{},[63,80591],{"alt":80587,"src":80592},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1770033519\u002Fblog\u002Fautomating-my-blog-workflow-with-github-copilot-sdk\u002Fhow-the-sdk-works_y3cm45",[27,80594,80595,80596,80603],{},"Here's something that confused me at first: the Copilot SDK doesn't make API calls directly to a cloud endpoint. Instead, it communicates with the ",[45,80597,80600],{"href":80598,"target":2716,"rel":80599},"https:\u002F\u002Fgithub.com\u002Ffeatures\u002Fcopilot\u002Fcli",[2718,2719],[42,80601,80602],{},"Copilot CLI"," running locally on your machine through JSON-RPC.",[27,80605,80606],{},"Think of it like this:",[128,80608,80611],{"className":80609,"code":80610,"language":5189},[5187],"Your Script → Copilot SDK → JSON-RPC → Copilot CLI → Copilot API\n",[22,80612,80610],{"__ignoreMap":133},[27,80614,80615],{},"This means you need to have the Copilot CLI installed and authenticated before you can use the SDK. On macOS, the setup looks like this:",[128,80617,80619],{"className":8665,"code":80618,"language":8667,"meta":133,"style":133},"brew install copilot-cli\nbrew install gh\ngh auth login\n",[22,80620,80621,80630,80639],{"__ignoreMap":133},[137,80622,80623,80625,80627],{"class":139,"line":140},[137,80624,56827],{"class":147},[137,80626,10268],{"class":284},[137,80628,80629],{"class":284}," copilot-cli\n",[137,80631,80632,80634,80636],{"class":139,"line":173},[137,80633,56827],{"class":147},[137,80635,10268],{"class":284},[137,80637,80638],{"class":284}," gh\n",[137,80640,80641,80643,80645],{"class":139,"line":188},[137,80642,79757],{"class":147},[137,80644,23716],{"class":284},[137,80646,80647],{"class":284}," login\n",[27,80649,4737,80650,80653],{},[22,80651,80652],{},"gh auth login"," command opens your browser and prompts you to authenticate with GitHub. Once authenticated, the Copilot CLI can use those credentials. The SDK then manages the CLI process lifecycle automatically by starting it when you create a client and stopping it when you're done.",[104,80655,80657],{"id":80656},"building-the-automation-script","Building the Automation Script",[27,80659,80660],{},"Now let's build the script. I'll focus on the essential parts and explain how they connect together.",[123,80662,47892],{"id":47891},[27,80664,80665,80666,80669,80670,894],{},"First, I created a ",[22,80667,80668],{},"scripts"," folder in my blog project with its own ",[22,80671,5140],{},[128,80673,80675],{"className":5155,"code":80674,"language":5157,"meta":133,"style":133},"{\n    \"name\": \"scripts\",\n    \"version\": \"1.0.0\",\n    \"type\": \"module\",\n    \"scripts\": {\n        \"convert\": \"tsx .\u002Fconvert-article.ts\"\n    },\n    \"devDependencies\": {\n        \"tsx\": \"^4.21.0\"\n    },\n    \"dependencies\": {\n        \"@github\u002Fcopilot-sdk\": \"^0.1.19\"\n    }\n}\n",[22,80676,80677,80681,80691,80701,80711,80717,80727,80731,80738,80748,80752,80758,80768,80772],{"__ignoreMap":133},[137,80678,80679],{"class":139,"line":140},[137,80680,15971],{"class":157},[137,80682,80683,80685,80687,80689],{"class":139,"line":173},[137,80684,79806],{"class":364},[137,80686,726],{"class":157},[137,80688,5164],{"class":284},[137,80690,1961],{"class":157},[137,80692,80693,80695,80697,80699],{"class":139,"line":188},[137,80694,79818],{"class":364},[137,80696,726],{"class":157},[137,80698,79823],{"class":284},[137,80700,1961],{"class":157},[137,80702,80703,80705,80707,80709],{"class":139,"line":269},[137,80704,57339],{"class":364},[137,80706,726],{"class":157},[137,80708,25777],{"class":284},[137,80710,1961],{"class":157},[137,80712,80713,80715],{"class":139,"line":278},[137,80714,57350],{"class":364},[137,80716,1819],{"class":157},[137,80718,80719,80722,80724],{"class":139,"line":291},[137,80720,80721],{"class":364},"        \"convert\"",[137,80723,726],{"class":157},[137,80725,80726],{"class":284},"\"tsx .\u002Fconvert-article.ts\"\n",[137,80728,80729],{"class":139,"line":297},[137,80730,775],{"class":157},[137,80732,80733,80736],{"class":139,"line":302},[137,80734,80735],{"class":364},"    \"devDependencies\"",[137,80737,1819],{"class":157},[137,80739,80740,80743,80745],{"class":139,"line":662},[137,80741,80742],{"class":364},"        \"tsx\"",[137,80744,726],{"class":157},[137,80746,80747],{"class":284},"\"^4.21.0\"\n",[137,80749,80750],{"class":139,"line":667},[137,80751,775],{"class":157},[137,80753,80754,80756],{"class":139,"line":786},[137,80755,79830],{"class":364},[137,80757,1819],{"class":157},[137,80759,80760,80763,80765],{"class":139,"line":798},[137,80761,80762],{"class":364},"        \"@github\u002Fcopilot-sdk\"",[137,80764,726],{"class":157},[137,80766,80767],{"class":284},"\"^0.1.19\"\n",[137,80769,80770],{"class":139,"line":803},[137,80771,294],{"class":157},[137,80773,80774],{"class":139,"line":931},[137,80775,510],{"class":157},[27,80777,4737,80778,80781,80782,80784,80785,80788],{},[22,80779,80780],{},"\"type\": \"module\""," enables ES modules, ",[22,80783,29200],{}," lets us run TypeScript directly, and ",[22,80786,80787],{},"@github\u002Fcopilot-sdk"," is the official Node.js SDK.",[123,80790,80792],{"id":80791},"the-scripts-workflow","The Script's Workflow",[27,80794,80795],{},"Before diving into code, here's what the script does at a high level:",[2569,80797,80798,80810,80816,80822,80828,80834,80840,80846],{},[1006,80799,80800,80803,80804,80807,80808,14105],{},[42,80801,80802],{},"Find the draft article"," - Look for any ",[22,80805,80806],{},".md"," file in the project root (excluding ",[22,80809,71763],{},[1006,80811,80812,80815],{},[42,80813,80814],{},"Gather context"," - Find recent blog articles to use as formatting examples",[1006,80817,80818,80821],{},[42,80819,80820],{},"Locate the tags component"," - My blog has a Vue component defining all available tags",[1006,80823,80824,80827],{},[42,80825,80826],{},"Build file attachments"," - Prepare the files to send to Copilot",[1006,80829,80830,80833],{},[42,80831,80832],{},"Build the prompt"," - Create detailed instructions for the conversion",[1006,80835,80836,80839],{},[42,80837,80838],{},"Send to Copilot"," - Use the SDK to send everything and wait for a response",[1006,80841,80842,80845],{},[42,80843,80844],{},"Parse and save"," - Extract the result and write the formatted article",[1006,80847,80848,80851],{},[42,80849,80850],{},"Clean up"," - Delete the original draft",[27,80853,80854],{},"I've written helper functions for steps 1-3 that handle the file system operations - finding the draft, walking the content directory for examples, and locating the tags component. These are straightforward Node.js file operations, so I won't show all the code here. The interesting part is how we interact with the Copilot SDK.",[123,80856,80858],{"id":80857},"building-file-attachments","Building File Attachments",[27,80860,80861],{},"One of the most powerful features of the SDK is file attachments. Instead of copying content into your prompt, you can reference files directly:",[128,80863,80865],{"className":13299,"code":80864,"language":13301,"meta":133,"style":133},"const attachments = [\n    { type: \"file\" as const, path: draftPath, displayName: \"draft-article.md\" },\n    { type: \"file\" as const, path: tagPillsPath, displayName: \"TagPills.vue\" },\n    ...exampleArticles.map((articlePath, index) => ({\n        type: \"file\" as const,\n        path: articlePath,\n        displayName: `example-article-${index + 1}.md`,\n    })),\n];\n",[22,80866,80867,80878,80898,80916,80941,80954,80959,80978,80983],{"__ignoreMap":133},[137,80868,80869,80871,80874,80876],{"class":139,"line":140},[137,80870,3077],{"class":143},[137,80872,80873],{"class":364}," attachments",[137,80875,151],{"class":143},[137,80877,49728],{"class":157},[137,80879,80880,80883,80886,80888,80890,80893,80896],{"class":139,"line":173},[137,80881,80882],{"class":157},"    { type: ",[137,80884,80885],{"class":284},"\"file\"",[137,80887,13881],{"class":143},[137,80889,20388],{"class":143},[137,80891,80892],{"class":157},", path: draftPath, displayName: ",[137,80894,80895],{"class":284},"\"draft-article.md\"",[137,80897,31293],{"class":157},[137,80899,80900,80902,80904,80906,80908,80911,80914],{"class":139,"line":188},[137,80901,80882],{"class":157},[137,80903,80885],{"class":284},[137,80905,13881],{"class":143},[137,80907,20388],{"class":143},[137,80909,80910],{"class":157},", path: tagPillsPath, displayName: ",[137,80912,80913],{"class":284},"\"TagPills.vue\"",[137,80915,31293],{"class":157},[137,80917,80918,80921,80924,80926,80928,80930,80932,80934,80936,80938],{"class":139,"line":269},[137,80919,80920],{"class":143},"    ...",[137,80922,80923],{"class":157},"exampleArticles.",[137,80925,37476],{"class":147},[137,80927,2774],{"class":157},[137,80929,49615],{"class":161},[137,80931,164],{"class":157},[137,80933,48711],{"class":161},[137,80935,219],{"class":157},[137,80937,222],{"class":143},[137,80939,80940],{"class":157}," ({\n",[137,80942,80943,80946,80948,80950,80952],{"class":139,"line":278},[137,80944,80945],{"class":157},"        type: ",[137,80947,80885],{"class":284},[137,80949,13881],{"class":143},[137,80951,20388],{"class":143},[137,80953,1961],{"class":157},[137,80955,80956],{"class":139,"line":291},[137,80957,80958],{"class":157},"        path: articlePath,\n",[137,80960,80961,80964,80967,80969,80971,80973,80976],{"class":139,"line":297},[137,80962,80963],{"class":157},"        displayName: ",[137,80965,80966],{"class":284},"`example-article-${",[137,80968,48711],{"class":157},[137,80970,361],{"class":143},[137,80972,8030],{"class":364},[137,80974,80975],{"class":284},"}.md`",[137,80977,1961],{"class":157},[137,80979,80980],{"class":139,"line":302},[137,80981,80982],{"class":157},"    })),\n",[137,80984,80985],{"class":139,"line":662},[137,80986,5727],{"class":157},[27,80988,80989,80990,80992,80993,80995,80996,80998,80999,81001],{},"Each attachment has a ",[22,80991,20355],{}," (always ",[22,80994,80885],{}," for files), a ",[22,80997,51291],{}," to the file on disk, and a ",[22,81000,20794],{}," that helps the model understand what it's looking at. In my case, I'm attaching:",[1003,81003,81004,81007,81014],{},[1006,81005,81006],{},"The draft article to convert",[1006,81008,81009,81010,81013],{},"My ",[22,81011,81012],{},"TagPills.vue"," component (so the model can extract available tags)",[1006,81015,81016],{},"Two recent blog articles as formatting examples",[27,81018,81019],{},"The model can then read and analyse these files to understand exactly what I need.",[123,81021,81023],{"id":81022},"crafting-the-prompt","Crafting the Prompt",[27,81025,81026],{},"The prompt is where you tell the model what to do. Here's a condensed version of mine:",[128,81028,81030],{"className":13299,"code":81029,"language":13301,"meta":133,"style":133},"function buildConversionPrompt(today: Date): string {\n    const dateStr = formatPublishedDate(today); \u002F\u002F e.g., \"2nd February 2026\"\n    const year = today.getFullYear();\n    const month = String(today.getMonth() + 1).padStart(2, \"0\");\n    const day = String(today.getDate()).padStart(2, \"0\");\n\n    return `You are a professional blog article converter. Your task is to convert a draft Markdown article into a fully structured blog article that matches the format and conventions of the example articles provided.\n\n## ATTACHED FILES\nI have attached the following files for context:\n1. **Draft Article** - The markdown file to convert\n2. **Example Articles** - Reference articles showing the expected format\n3. **TagPills.vue** - Vue component containing all available article tags\n\n## TODAY'S DATE INFORMATION\n- Published date format: \"${dateStr}\"\n- Target folder structure: content\u002F${year}\u002F${month}\u002F${day}\u002F\n\n## CONVERSION RULES\n[... detailed rules for frontmatter, images, links, components ...]\n\n## REQUIRED OUTPUT FORMAT\nYou MUST respond with a JSON object containing exactly these two fields:\n1. \"slug\": The generated slug for the filename (without .md extension)\n2. \"content\": The complete converted Markdown article content\n\nExample response format:\n{\n  \"slug\": \"my-article-slug\",\n  \"content\": \"---\\\\ntitle: My Article\\\\n...rest of the article...\"\n}\n\nIMPORTANT: Output ONLY the JSON object, no additional text or explanation.`;\n}\n",[22,81031,81032,81056,81074,81091,81131,81162,81166,81173,81177,81182,81187,81192,81197,81202,81206,81211,81222,81244,81248,81253,81258,81262,81267,81272,81277,81282,81286,81291,81295,81300,81316,81320,81324,81331],{"__ignoreMap":133},[137,81033,81034,81036,81039,81041,81044,81046,81048,81050,81052,81054],{"class":139,"line":140},[137,81035,483],{"class":143},[137,81037,81038],{"class":147}," buildConversionPrompt",[137,81040,356],{"class":157},[137,81042,81043],{"class":161},"today",[137,81045,894],{"class":143},[137,81047,77686],{"class":147},[137,81049,14105],{"class":157},[137,81051,894],{"class":143},[137,81053,13630],{"class":364},[137,81055,256],{"class":157},[137,81057,81058,81060,81063,81065,81068,81071],{"class":139,"line":173},[137,81059,4177],{"class":143},[137,81061,81062],{"class":364}," dateStr",[137,81064,151],{"class":143},[137,81066,81067],{"class":147}," formatPublishedDate",[137,81069,81070],{"class":157},"(today); ",[137,81072,81073],{"class":308},"\u002F\u002F e.g., \"2nd February 2026\"\n",[137,81075,81076,81078,81081,81083,81086,81089],{"class":139,"line":188},[137,81077,4177],{"class":143},[137,81079,81080],{"class":364}," year",[137,81082,151],{"class":143},[137,81084,81085],{"class":157}," today.",[137,81087,81088],{"class":147},"getFullYear",[137,81090,924],{"class":157},[137,81092,81093,81095,81098,81100,81103,81106,81109,81111,81113,81115,81117,81120,81122,81124,81126,81129],{"class":139,"line":269},[137,81094,4177],{"class":143},[137,81096,81097],{"class":364}," month",[137,81099,151],{"class":143},[137,81101,81102],{"class":147}," String",[137,81104,81105],{"class":157},"(today.",[137,81107,81108],{"class":147},"getMonth",[137,81110,3348],{"class":157},[137,81112,182],{"class":143},[137,81114,8030],{"class":364},[137,81116,4409],{"class":157},[137,81118,81119],{"class":147},"padStart",[137,81121,356],{"class":157},[137,81123,10345],{"class":364},[137,81125,164],{"class":157},[137,81127,81128],{"class":284},"\"0\"",[137,81130,1502],{"class":157},[137,81132,81133,81135,81138,81140,81142,81144,81147,81150,81152,81154,81156,81158,81160],{"class":139,"line":278},[137,81134,4177],{"class":143},[137,81136,81137],{"class":364}," day",[137,81139,151],{"class":143},[137,81141,81102],{"class":147},[137,81143,81105],{"class":157},[137,81145,81146],{"class":147},"getDate",[137,81148,81149],{"class":157},"()).",[137,81151,81119],{"class":147},[137,81153,356],{"class":157},[137,81155,10345],{"class":364},[137,81157,164],{"class":157},[137,81159,81128],{"class":284},[137,81161,1502],{"class":157},[137,81163,81164],{"class":139,"line":291},[137,81165,516],{"emptyLinePlaceholder":515},[137,81167,81168,81170],{"class":139,"line":297},[137,81169,176],{"class":143},[137,81171,81172],{"class":284}," `You are a professional blog article converter. Your task is to convert a draft Markdown article into a fully structured blog article that matches the format and conventions of the example articles provided.\n",[137,81174,81175],{"class":139,"line":302},[137,81176,516],{"emptyLinePlaceholder":515},[137,81178,81179],{"class":139,"line":662},[137,81180,81181],{"class":284},"## ATTACHED FILES\n",[137,81183,81184],{"class":139,"line":667},[137,81185,81186],{"class":284},"I have attached the following files for context:\n",[137,81188,81189],{"class":139,"line":786},[137,81190,81191],{"class":284},"1. **Draft Article** - The markdown file to convert\n",[137,81193,81194],{"class":139,"line":798},[137,81195,81196],{"class":284},"2. **Example Articles** - Reference articles showing the expected format\n",[137,81198,81199],{"class":139,"line":803},[137,81200,81201],{"class":284},"3. **TagPills.vue** - Vue component containing all available article tags\n",[137,81203,81204],{"class":139,"line":931},[137,81205,516],{"emptyLinePlaceholder":515},[137,81207,81208],{"class":139,"line":1568},[137,81209,81210],{"class":284},"## TODAY'S DATE INFORMATION\n",[137,81212,81213,81216,81219],{"class":139,"line":1573},[137,81214,81215],{"class":284},"- Published date format: \"${",[137,81217,81218],{"class":157},"dateStr",[137,81220,81221],{"class":284},"}\"\n",[137,81223,81224,81227,81230,81233,81236,81238,81241],{"class":139,"line":1578},[137,81225,81226],{"class":284},"- Target folder structure: content\u002F${",[137,81228,81229],{"class":157},"year",[137,81231,81232],{"class":284},"}\u002F${",[137,81234,81235],{"class":157},"month",[137,81237,81232],{"class":284},[137,81239,81240],{"class":157},"day",[137,81242,81243],{"class":284},"}\u002F\n",[137,81245,81246],{"class":139,"line":1588},[137,81247,516],{"emptyLinePlaceholder":515},[137,81249,81250],{"class":139,"line":1601},[137,81251,81252],{"class":284},"## CONVERSION RULES\n",[137,81254,81255],{"class":139,"line":3802},[137,81256,81257],{"class":284},"[... detailed rules for frontmatter, images, links, components ...]\n",[137,81259,81260],{"class":139,"line":3808},[137,81261,516],{"emptyLinePlaceholder":515},[137,81263,81264],{"class":139,"line":3822},[137,81265,81266],{"class":284},"## REQUIRED OUTPUT FORMAT\n",[137,81268,81269],{"class":139,"line":3827},[137,81270,81271],{"class":284},"You MUST respond with a JSON object containing exactly these two fields:\n",[137,81273,81274],{"class":139,"line":3832},[137,81275,81276],{"class":284},"1. \"slug\": The generated slug for the filename (without .md extension)\n",[137,81278,81279],{"class":139,"line":3840},[137,81280,81281],{"class":284},"2. \"content\": The complete converted Markdown article content\n",[137,81283,81284],{"class":139,"line":3846},[137,81285,516],{"emptyLinePlaceholder":515},[137,81287,81288],{"class":139,"line":3861},[137,81289,81290],{"class":284},"Example response format:\n",[137,81292,81293],{"class":139,"line":3883},[137,81294,15971],{"class":284},[137,81296,81297],{"class":139,"line":3896},[137,81298,81299],{"class":284},"  \"slug\": \"my-article-slug\",\n",[137,81301,81302,81305,81308,81311,81313],{"class":139,"line":3901},[137,81303,81304],{"class":284},"  \"content\": \"---",[137,81306,81307],{"class":364},"\\\\",[137,81309,81310],{"class":284},"ntitle: My Article",[137,81312,81307],{"class":364},[137,81314,81315],{"class":284},"n...rest of the article...\"\n",[137,81317,81318],{"class":139,"line":3906},[137,81319,510],{"class":284},[137,81321,81322],{"class":139,"line":3911},[137,81323,516],{"emptyLinePlaceholder":515},[137,81325,81326,81329],{"class":139,"line":4666},[137,81327,81328],{"class":284},"IMPORTANT: Output ONLY the JSON object, no additional text or explanation.`",[137,81330,3276],{"class":157},[137,81332,81333],{"class":139,"line":4672},[137,81334,510],{"class":157},[27,81336,81337],{},"I've omitted the full conversion rules for brevity, but they include specifics about:",[1003,81339,81340,81343,81346,81349,81352],{},[1006,81341,81342],{},"How to generate URL-friendly slugs from titles",[1006,81344,81345],{},"The exact frontmatter fields required (title, description, keywords, tags, etc.)",[1006,81347,81348],{},"Cloudinary image modifier patterns for different image sizes",[1006,81350,81351],{},"How external links should be formatted",[1006,81353,81354],{},"Which components to include and which to avoid",[27,81356,81357,81358,81361],{},"The key insight is that ",[42,81359,81360],{},"the more specific your prompt, the more consistent your results",". By requesting JSON output with a specific structure, parsing the response becomes straightforward.",[123,81363,81365],{"id":81364},"initialising-and-using-the-sdk","Initialising and Using the SDK",[27,81367,81368],{},"Here's the core of the script - the actual interaction with Copilot:",[128,81370,81372],{"className":13299,"code":81371,"language":13301,"meta":133,"style":133},"import { CopilotClient } from \"@github\u002Fcopilot-sdk\";\n\nasync function main() {\n    \u002F\u002F ... file discovery and preparation code ...\n\n    const client = new CopilotClient();\n\n    try {\n        \u002F\u002F Start the client (this launches the Copilot CLI)\n        await client.start();\n        console.log(\"Copilot client started successfully.\");\n\n        \u002F\u002F Create a session with your preferred model\n        const session = await client.createSession({\n            model: \"claude-opus-4.5\",\n        });\n        console.log(\"Session created with model: claude-opus-4.5\");\n\n        \u002F\u002F Send the prompt with file attachments\n        const result = await session.sendAndWait(\n            {\n                prompt,\n                attachments,\n            },\n            300000, \u002F\u002F 5 minutes timeout\n        );\n\n        \u002F\u002F Extract the response text\n        if (!result || !result.data || !result.data.content) {\n            throw new Error(\"No response received from Copilot\");\n        }\n\n        const responseText = result.data.content;\n\n        \u002F\u002F Parse the JSON response\n        const parsed = parseResponse(responseText);\n        const { slug, content } = parsed;\n\n        \u002F\u002F Write the converted article to the correct location\n        const targetFile = path.join(contentDir, year, month, day, `${slug}.md`);\n        fs.writeFileSync(targetFile, content, \"utf-8\");\n        console.log(`Article created: ${targetFile}`);\n\n        \u002F\u002F Clean up\n        await session.destroy();\n        await client.stop();\n        process.exit(0);\n    } catch (error) {\n        console.error(\"Error during conversion:\", error);\n        await client.stop();\n        process.exit(1);\n    }\n}\n",[22,81373,81374,81388,81392,81402,81407,81411,81426,81430,81436,81441,81451,81464,81468,81473,81491,81501,81505,81518,81522,81527,81545,81550,81555,81560,81564,81574,81578,81582,81587,81612,81627,81631,81635,81647,81651,81656,81671,81691,81695,81700,81724,81737,81755,81759,81764,81774,81785,81797,81805,81818,81828,81840,81844],{"__ignoreMap":133},[137,81375,81376,81378,81381,81383,81386],{"class":139,"line":140},[137,81377,10287],{"class":143},[137,81379,81380],{"class":157}," { CopilotClient } ",[137,81382,10954],{"class":143},[137,81384,81385],{"class":284}," \"@github\u002Fcopilot-sdk\"",[137,81387,3276],{"class":157},[137,81389,81390],{"class":139,"line":173},[137,81391,516],{"emptyLinePlaceholder":515},[137,81393,81394,81396,81398,81400],{"class":139,"line":188},[137,81395,15050],{"class":143},[137,81397,154],{"class":143},[137,81399,58463],{"class":147},[137,81401,275],{"class":157},[137,81403,81404],{"class":139,"line":269},[137,81405,81406],{"class":308},"    \u002F\u002F ... file discovery and preparation code ...\n",[137,81408,81409],{"class":139,"line":278},[137,81410,516],{"emptyLinePlaceholder":515},[137,81412,81413,81415,81417,81419,81421,81424],{"class":139,"line":291},[137,81414,4177],{"class":143},[137,81416,15264],{"class":364},[137,81418,151],{"class":143},[137,81420,1426],{"class":143},[137,81422,81423],{"class":147}," CopilotClient",[137,81425,924],{"class":157},[137,81427,81428],{"class":139,"line":297},[137,81429,516],{"emptyLinePlaceholder":515},[137,81431,81432,81434],{"class":139,"line":302},[137,81433,25035],{"class":143},[137,81435,256],{"class":157},[137,81437,81438],{"class":139,"line":662},[137,81439,81440],{"class":308},"        \u002F\u002F Start the client (this launches the Copilot CLI)\n",[137,81442,81443,81445,81447,81449],{"class":139,"line":667},[137,81444,25043],{"class":143},[137,81446,75571],{"class":157},[137,81448,59360],{"class":147},[137,81450,924],{"class":157},[137,81452,81453,81455,81457,81459,81462],{"class":139,"line":786},[137,81454,350],{"class":157},[137,81456,353],{"class":147},[137,81458,356],{"class":157},[137,81460,81461],{"class":284},"\"Copilot client started successfully.\"",[137,81463,1502],{"class":157},[137,81465,81466],{"class":139,"line":798},[137,81467,516],{"emptyLinePlaceholder":515},[137,81469,81470],{"class":139,"line":803},[137,81471,81472],{"class":308},"        \u002F\u002F Create a session with your preferred model\n",[137,81474,81475,81477,81480,81482,81484,81486,81489],{"class":139,"line":931},[137,81476,3008],{"class":143},[137,81478,81479],{"class":364}," session",[137,81481,151],{"class":143},[137,81483,15069],{"class":143},[137,81485,75571],{"class":157},[137,81487,81488],{"class":147},"createSession",[137,81490,3175],{"class":157},[137,81492,81493,81496,81499],{"class":139,"line":1568},[137,81494,81495],{"class":157},"            model: ",[137,81497,81498],{"class":284},"\"claude-opus-4.5\"",[137,81500,1961],{"class":157},[137,81502,81503],{"class":139,"line":1573},[137,81504,14079],{"class":157},[137,81506,81507,81509,81511,81513,81516],{"class":139,"line":1578},[137,81508,350],{"class":157},[137,81510,353],{"class":147},[137,81512,356],{"class":157},[137,81514,81515],{"class":284},"\"Session created with model: claude-opus-4.5\"",[137,81517,1502],{"class":157},[137,81519,81520],{"class":139,"line":1588},[137,81521,516],{"emptyLinePlaceholder":515},[137,81523,81524],{"class":139,"line":1601},[137,81525,81526],{"class":308},"        \u002F\u002F Send the prompt with file attachments\n",[137,81528,81529,81531,81533,81535,81537,81540,81543],{"class":139,"line":3802},[137,81530,3008],{"class":143},[137,81532,26939],{"class":364},[137,81534,151],{"class":143},[137,81536,15069],{"class":143},[137,81538,81539],{"class":157}," session.",[137,81541,81542],{"class":147},"sendAndWait",[137,81544,11813],{"class":157},[137,81546,81547],{"class":139,"line":3808},[137,81548,81549],{"class":157},"            {\n",[137,81551,81552],{"class":139,"line":3822},[137,81553,81554],{"class":157},"                prompt,\n",[137,81556,81557],{"class":139,"line":3827},[137,81558,81559],{"class":157},"                attachments,\n",[137,81561,81562],{"class":139,"line":3832},[137,81563,14074],{"class":157},[137,81565,81566,81569,81571],{"class":139,"line":3840},[137,81567,81568],{"class":364},"            300000",[137,81570,164],{"class":157},[137,81572,81573],{"class":308},"\u002F\u002F 5 minutes timeout\n",[137,81575,81576],{"class":139,"line":3846},[137,81577,49105],{"class":157},[137,81579,81580],{"class":139,"line":3861},[137,81581,516],{"emptyLinePlaceholder":515},[137,81583,81584],{"class":139,"line":3883},[137,81585,81586],{"class":308},"        \u002F\u002F Extract the response text\n",[137,81588,81589,81591,81593,81595,81598,81600,81602,81605,81607,81609],{"class":139,"line":3896},[137,81590,5496],{"class":143},[137,81592,158],{"class":157},[137,81594,17393],{"class":143},[137,81596,81597],{"class":157},"result ",[137,81599,50706],{"class":143},[137,81601,27133],{"class":143},[137,81603,81604],{"class":157},"result.data ",[137,81606,50706],{"class":143},[137,81608,27133],{"class":143},[137,81610,81611],{"class":157},"result.data.content) {\n",[137,81613,81614,81616,81618,81620,81622,81625],{"class":139,"line":3901},[137,81615,15739],{"class":143},[137,81617,1426],{"class":143},[137,81619,40520],{"class":147},[137,81621,356],{"class":157},[137,81623,81624],{"class":284},"\"No response received from Copilot\"",[137,81626,1502],{"class":157},[137,81628,81629],{"class":139,"line":3906},[137,81630,1966],{"class":157},[137,81632,81633],{"class":139,"line":3911},[137,81634,516],{"emptyLinePlaceholder":515},[137,81636,81637,81639,81642,81644],{"class":139,"line":4666},[137,81638,3008],{"class":143},[137,81640,81641],{"class":364}," responseText",[137,81643,151],{"class":143},[137,81645,81646],{"class":157}," result.data.content;\n",[137,81648,81649],{"class":139,"line":4672},[137,81650,516],{"emptyLinePlaceholder":515},[137,81652,81653],{"class":139,"line":4680},[137,81654,81655],{"class":308},"        \u002F\u002F Parse the JSON response\n",[137,81657,81658,81660,81663,81665,81668],{"class":139,"line":4711},[137,81659,3008],{"class":143},[137,81661,81662],{"class":364}," parsed",[137,81664,151],{"class":143},[137,81666,81667],{"class":147}," parseResponse",[137,81669,81670],{"class":157},"(responseText);\n",[137,81672,81673,81675,81677,81680,81682,81684,81686,81688],{"class":139,"line":4716},[137,81674,3008],{"class":143},[137,81676,8906],{"class":157},[137,81678,81679],{"class":364},"slug",[137,81681,164],{"class":157},[137,81683,29728],{"class":364},[137,81685,8911],{"class":157},[137,81687,253],{"class":143},[137,81689,81690],{"class":157}," parsed;\n",[137,81692,81693],{"class":139,"line":4721},[137,81694,516],{"emptyLinePlaceholder":515},[137,81696,81697],{"class":139,"line":4727},[137,81698,81699],{"class":308},"        \u002F\u002F Write the converted article to the correct location\n",[137,81701,81702,81704,81707,81709,81711,81713,81716,81718,81720,81722],{"class":139,"line":4732},[137,81703,3008],{"class":143},[137,81705,81706],{"class":364}," targetFile",[137,81708,151],{"class":143},[137,81710,61989],{"class":157},[137,81712,8628],{"class":147},[137,81714,81715],{"class":157},"(contentDir, year, month, day, ",[137,81717,18820],{"class":284},[137,81719,81679],{"class":157},[137,81721,80975],{"class":284},[137,81723,1502],{"class":157},[137,81725,81726,81728,81730,81733,81735],{"class":139,"line":5006},[137,81727,48630],{"class":157},[137,81729,58177],{"class":147},[137,81731,81732],{"class":157},"(targetFile, content, ",[137,81734,48601],{"class":284},[137,81736,1502],{"class":157},[137,81738,81739,81741,81743,81745,81748,81751,81753],{"class":139,"line":5014},[137,81740,350],{"class":157},[137,81742,353],{"class":147},[137,81744,356],{"class":157},[137,81746,81747],{"class":284},"`Article created: ${",[137,81749,81750],{"class":157},"targetFile",[137,81752,4706],{"class":284},[137,81754,1502],{"class":157},[137,81756,81757],{"class":139,"line":14343},[137,81758,516],{"emptyLinePlaceholder":515},[137,81760,81761],{"class":139,"line":24199},[137,81762,81763],{"class":308},"        \u002F\u002F Clean up\n",[137,81765,81766,81768,81770,81772],{"class":139,"line":24773},[137,81767,25043],{"class":143},[137,81769,81539],{"class":157},[137,81771,31881],{"class":147},[137,81773,924],{"class":157},[137,81775,81776,81778,81780,81783],{"class":139,"line":24778},[137,81777,25043],{"class":143},[137,81779,75571],{"class":157},[137,81781,81782],{"class":147},"stop",[137,81784,924],{"class":157},[137,81786,81787,81789,81791,81793,81795],{"class":139,"line":24783},[137,81788,58524],{"class":157},[137,81790,58527],{"class":147},[137,81792,356],{"class":157},[137,81794,6044],{"class":364},[137,81796,1502],{"class":157},[137,81798,81799,81801,81803],{"class":139,"line":24793},[137,81800,24944],{"class":157},[137,81802,2807],{"class":143},[137,81804,15734],{"class":157},[137,81806,81807,81809,81811,81813,81816],{"class":139,"line":24827},[137,81808,350],{"class":157},[137,81810,2812],{"class":147},[137,81812,356],{"class":157},[137,81814,81815],{"class":284},"\"Error during conversion:\"",[137,81817,17836],{"class":157},[137,81819,81820,81822,81824,81826],{"class":139,"line":24857},[137,81821,25043],{"class":143},[137,81823,75571],{"class":157},[137,81825,81782],{"class":147},[137,81827,924],{"class":157},[137,81829,81830,81832,81834,81836,81838],{"class":139,"line":24862},[137,81831,58524],{"class":157},[137,81833,58527],{"class":147},[137,81835,356],{"class":157},[137,81837,6065],{"class":364},[137,81839,1502],{"class":157},[137,81841,81842],{"class":139,"line":24867},[137,81843,294],{"class":157},[137,81845,81846],{"class":139,"line":24884},[137,81847,510],{"class":157},[27,81849,81850],{},"Let me break down the key SDK operations:",[27,81852,81853],{},[42,81854,81855],{},"Starting the Client:",[128,81857,81859],{"className":13299,"code":81858,"language":13301,"meta":133,"style":133},"const client = new CopilotClient();\nawait client.start();\n",[22,81860,81861,81875],{"__ignoreMap":133},[137,81862,81863,81865,81867,81869,81871,81873],{"class":139,"line":140},[137,81864,3077],{"class":143},[137,81866,15264],{"class":364},[137,81868,151],{"class":143},[137,81870,1426],{"class":143},[137,81872,81423],{"class":147},[137,81874,924],{"class":157},[137,81876,81877,81879,81881,81883],{"class":139,"line":173},[137,81878,54992],{"class":143},[137,81880,75571],{"class":157},[137,81882,59360],{"class":147},[137,81884,924],{"class":157},[27,81886,81887],{},"This creates a client instance and starts the Copilot CLI in server mode. The SDK handles all the process management for you.",[27,81889,81890],{},[42,81891,81892],{},"Creating a Session:",[128,81894,81896],{"className":13299,"code":81895,"language":13301,"meta":133,"style":133},"const session = await client.createSession({\n    model: \"claude-opus-4.5\",\n});\n",[22,81897,81898,81914,81922],{"__ignoreMap":133},[137,81899,81900,81902,81904,81906,81908,81910,81912],{"class":139,"line":140},[137,81901,3077],{"class":143},[137,81903,81479],{"class":364},[137,81905,151],{"class":143},[137,81907,15069],{"class":143},[137,81909,75571],{"class":157},[137,81911,81488],{"class":147},[137,81913,3175],{"class":157},[137,81915,81916,81918,81920],{"class":139,"line":173},[137,81917,57491],{"class":157},[137,81919,81498],{"class":284},[137,81921,1961],{"class":157},[137,81923,81924],{"class":139,"line":188},[137,81925,5422],{"class":157},[27,81927,81928,81929,81932,81933,81936],{},"A session represents a conversation. You can choose which model to use - I picked ",[22,81930,81931],{},"claude-opus-4.5",", but ",[22,81934,81935],{},"gpt-5"," and other models are also available.",[27,81938,81939],{},[42,81940,81941],{},"Sending with Attachments:",[128,81943,81945],{"className":13299,"code":81944,"language":13301,"meta":133,"style":133},"const result = await session.sendAndWait({ prompt, attachments }, 300000);\n",[22,81946,81947],{"__ignoreMap":133},[137,81948,81949,81951,81953,81955,81957,81959,81961,81964,81967],{"class":139,"line":140},[137,81950,3077],{"class":143},[137,81952,26939],{"class":364},[137,81954,151],{"class":143},[137,81956,15069],{"class":143},[137,81958,81539],{"class":157},[137,81960,81542],{"class":147},[137,81962,81963],{"class":157},"({ prompt, attachments }, ",[137,81965,81966],{"class":364},"300000",[137,81968,1502],{"class":157},[27,81970,4737,81971,81973],{},[22,81972,81542],{}," method sends your prompt along with any file attachments and waits for the complete response. The second argument is a timeout in milliseconds - I use 5 minutes since processing multiple files can take a while.",[27,81975,81976],{},[42,81977,81978],{},"Extracting the Response:",[128,81980,81982],{"className":13299,"code":81981,"language":13301,"meta":133,"style":133},"const responseText = result.data.content;\n",[22,81983,81984],{"__ignoreMap":133},[137,81985,81986,81988,81990,81992],{"class":139,"line":140},[137,81987,3077],{"class":143},[137,81989,81641],{"class":364},[137,81991,151],{"class":143},[137,81993,81646],{"class":157},[27,81995,81996,81997,1017],{},"The response is an event object where the actual text lives in ",[22,81998,81999],{},"result.data.content",[27,82001,82002],{},[42,82003,82004],{},"Cleanup:",[128,82006,82008],{"className":13299,"code":82007,"language":13301,"meta":133,"style":133},"await session.destroy();\nawait client.stop();\nprocess.exit(0);\n",[22,82009,82010,82020,82030],{"__ignoreMap":133},[137,82011,82012,82014,82016,82018],{"class":139,"line":140},[137,82013,54992],{"class":143},[137,82015,81539],{"class":157},[137,82017,31881],{"class":147},[137,82019,924],{"class":157},[137,82021,82022,82024,82026,82028],{"class":139,"line":173},[137,82023,54992],{"class":143},[137,82025,75571],{"class":157},[137,82027,81782],{"class":147},[137,82029,924],{"class":157},[137,82031,82032,82035,82037,82039,82041],{"class":139,"line":188},[137,82033,82034],{"class":157},"process.",[137,82036,58527],{"class":147},[137,82038,356],{"class":157},[137,82040,6044],{"class":364},[137,82042,1502],{"class":157},[27,82044,82045,82046,82049],{},"Always clean up by destroying the session and stopping the client. The ",[22,82047,82048],{},"process.exit(0)"," ensures the script terminates - without it, open handles might keep the process running.",[123,82051,82053],{"id":82052},"parsing-the-response","Parsing the Response",[27,82055,82056],{},"Since I asked for JSON output, I need a function to parse it:",[128,82058,82060],{"className":13299,"code":82059,"language":13301,"meta":133,"style":133},"function parseResponse(response: string): { slug: string; content: string } | null {\n    try {\n        \u002F\u002F Try to extract JSON from the response (in case of extra text)\n        const jsonMatch = response.match(\u002F\\{[\\s\\S]*\"slug\"[\\s\\S]*\"content\"[\\s\\S]*\\}\u002F);\n        if (jsonMatch) {\n            const parsed = JSON.parse(jsonMatch[0]);\n            if (parsed.slug && parsed.content) {\n                return { slug: parsed.slug, content: parsed.content };\n            }\n        }\n\n        \u002F\u002F Fallback: try parsing the entire response\n        const parsed = JSON.parse(response);\n        if (parsed.slug && parsed.content) {\n            return { slug: parsed.slug, content: parsed.content };\n        }\n    } catch (e) {\n        console.error(\"Failed to parse JSON response:\", e);\n    }\n    return null;\n}\n",[22,82061,82062,82104,82110,82115,82157,82164,82185,82197,82204,82208,82212,82216,82221,82237,82247,82253,82257,82265,82279,82283,82291],{"__ignoreMap":133},[137,82063,82064,82066,82068,82070,82072,82074,82076,82078,82080,82082,82084,82086,82088,82090,82092,82094,82096,82098,82100,82102],{"class":139,"line":140},[137,82065,483],{"class":143},[137,82067,81667],{"class":147},[137,82069,356],{"class":157},[137,82071,59756],{"class":161},[137,82073,894],{"class":143},[137,82075,13630],{"class":364},[137,82077,14105],{"class":157},[137,82079,894],{"class":143},[137,82081,8906],{"class":157},[137,82083,81679],{"class":161},[137,82085,894],{"class":143},[137,82087,13630],{"class":364},[137,82089,2323],{"class":157},[137,82091,29728],{"class":161},[137,82093,894],{"class":143},[137,82095,13630],{"class":364},[137,82097,8911],{"class":157},[137,82099,7684],{"class":143},[137,82101,3417],{"class":364},[137,82103,256],{"class":157},[137,82105,82106,82108],{"class":139,"line":173},[137,82107,25035],{"class":143},[137,82109,256],{"class":157},[137,82111,82112],{"class":139,"line":188},[137,82113,82114],{"class":308},"        \u002F\u002F Try to extract JSON from the response (in case of extra text)\n",[137,82116,82117,82119,82122,82124,82126,82128,82130,82132,82134,82136,82138,82141,82143,82145,82147,82149,82151,82153,82155],{"class":139,"line":269},[137,82118,3008],{"class":143},[137,82120,82121],{"class":364}," jsonMatch",[137,82123,151],{"class":143},[137,82125,57802],{"class":157},[137,82127,58083],{"class":147},[137,82129,356],{"class":157},[137,82131,47],{"class":284},[137,82133,58103],{"class":58102},[137,82135,58106],{"class":364},[137,82137,7672],{"class":143},[137,82139,82140],{"class":14746},"\"slug\"",[137,82142,58106],{"class":364},[137,82144,7672],{"class":143},[137,82146,67608],{"class":14746},[137,82148,58106],{"class":364},[137,82150,7672],{"class":143},[137,82152,58111],{"class":58102},[137,82154,47],{"class":284},[137,82156,1502],{"class":157},[137,82158,82159,82161],{"class":139,"line":278},[137,82160,5496],{"class":143},[137,82162,82163],{"class":157}," (jsonMatch) {\n",[137,82165,82166,82168,82170,82172,82174,82176,82178,82181,82183],{"class":139,"line":291},[137,82167,5772],{"class":143},[137,82169,81662],{"class":364},[137,82171,151],{"class":143},[137,82173,17436],{"class":364},[137,82175,1017],{"class":157},[137,82177,17441],{"class":147},[137,82179,82180],{"class":157},"(jsonMatch[",[137,82182,6044],{"class":364},[137,82184,43556],{"class":157},[137,82186,82187,82189,82192,82194],{"class":139,"line":297},[137,82188,5747],{"class":143},[137,82190,82191],{"class":157}," (parsed.slug ",[137,82193,3351],{"class":143},[137,82195,82196],{"class":157}," parsed.content) {\n",[137,82198,82199,82201],{"class":139,"line":302},[137,82200,5761],{"class":143},[137,82202,82203],{"class":157}," { slug: parsed.slug, content: parsed.content };\n",[137,82205,82206],{"class":139,"line":662},[137,82207,760],{"class":157},[137,82209,82210],{"class":139,"line":667},[137,82211,1966],{"class":157},[137,82213,82214],{"class":139,"line":786},[137,82215,516],{"emptyLinePlaceholder":515},[137,82217,82218],{"class":139,"line":798},[137,82219,82220],{"class":308},"        \u002F\u002F Fallback: try parsing the entire response\n",[137,82222,82223,82225,82227,82229,82231,82233,82235],{"class":139,"line":803},[137,82224,3008],{"class":143},[137,82226,81662],{"class":364},[137,82228,151],{"class":143},[137,82230,17436],{"class":364},[137,82232,1017],{"class":157},[137,82234,17441],{"class":147},[137,82236,68522],{"class":157},[137,82238,82239,82241,82243,82245],{"class":139,"line":931},[137,82240,5496],{"class":143},[137,82242,82191],{"class":157},[137,82244,3351],{"class":143},[137,82246,82196],{"class":157},[137,82248,82249,82251],{"class":139,"line":1568},[137,82250,4683],{"class":143},[137,82252,82203],{"class":157},[137,82254,82255],{"class":139,"line":1573},[137,82256,1966],{"class":157},[137,82258,82259,82261,82263],{"class":139,"line":1578},[137,82260,24944],{"class":157},[137,82262,2807],{"class":143},[137,82264,62559],{"class":157},[137,82266,82267,82269,82271,82273,82276],{"class":139,"line":1588},[137,82268,350],{"class":157},[137,82270,2812],{"class":147},[137,82272,356],{"class":157},[137,82274,82275],{"class":284},"\"Failed to parse JSON response:\"",[137,82277,82278],{"class":157},", e);\n",[137,82280,82281],{"class":139,"line":1601},[137,82282,294],{"class":157},[137,82284,82285,82287,82289],{"class":139,"line":3802},[137,82286,176],{"class":143},[137,82288,3417],{"class":364},[137,82290,3276],{"class":157},[137,82292,82293],{"class":139,"line":3808},[137,82294,510],{"class":157},[27,82296,82297],{},"This is intentionally defensive - it first tries to extract JSON using a regex (in case the model added extra text despite instructions), then falls back to parsing the whole response. LLMs don't always follow instructions perfectly, so a bit of flexibility helps.",[104,82299,82301],{"id":82300},"running-the-script","Running the Script",[27,82303,82304],{},"With everything in place, converting an article is now a single command:",[128,82306,82308],{"className":8665,"code":82307,"language":8667,"meta":133,"style":133},"cd scripts\nnpm run convert\n",[22,82309,82310,82317],{"__ignoreMap":133},[137,82311,82312,82314],{"class":139,"line":140},[137,82313,9558],{"class":364},[137,82315,82316],{"class":284}," scripts\n",[137,82318,82319,82321,82323],{"class":139,"line":173},[137,82320,9536],{"class":147},[137,82322,9578],{"class":284},[137,82324,82325],{"class":284}," convert\n",[27,82327,82328],{},"The output walks through each step:",[128,82330,82333],{"className":82331,"code":82332,"language":5189},[5187],"Starting Article Conversion with Copilot SDK\nRoot directory: \u002Fpath\u002Fto\u002Fmy-blog\nContent directory: \u002Fpath\u002Fto\u002Fmy-blog\u002Fcontent\n\nStep 1: Finding draft article in root...\nFound draft article: \u002Fpath\u002Fto\u002Fmy-blog\u002FMy New Article.md\n\nStep 2: Finding example articles for context...\nFound 2 example articles:\n  - content\u002F2026\u002F01\u002F18\u002Funderstanding-modern-rpc-frameworks.md\n  - content\u002F2026\u002F01\u002F04\u002Fblogging-in-2026.md\n\nStep 3: Locating TagPills component...\nFound TagPills component: \u002Fpath\u002Fto\u002Fmy-blog\u002Fapp\u002Fcomponents\u002FTagPills.vue\n\nStep 4: Preparing file attachments...\nPrepared 4 file attachments\n\nStep 5: Building conversion prompt...\n\nStep 6: Initializing Copilot SDK...\nCopilot client started successfully.\nSession created with model: claude-opus-4.5\n\nStep 7: Sending prompt to Copilot with file attachments...\n\nStep 8: Parsing Copilot response...\nGenerated slug: my-new-article\n\nStep 9: Creating article file...\nArticle created: \u002Fpath\u002Fto\u002Fmy-blog\u002Fcontent\u002F2026\u002F02\u002F02\u002Fmy-new-article.md\n\nStep 10: Cleaning up...\nDeleted original draft: \u002Fpath\u002Fto\u002Fmy-blog\u002FMy New Article.md\n\nConversion completed successfully!\nNew article: content\u002F2026\u002F02\u002F02\u002Fmy-new-article.md\n",[22,82334,82332],{"__ignoreMap":133},[27,82336,82337],{},"What used to take several minutes of manual prompting and copying now happens in under a minute. I drop my draft Markdown file in the project root, include the Cloudinary image URLs at the bottom, run the script, and it's done.",[104,82339,82341],{"id":82340},"a-note-on-cicd","A Note on CI\u002FCD",[27,82343,82344],{},"I did experiment with running this script in a GitHub Actions workflow. The idea was appealing - push a draft to a branch, have it automatically converted, then trigger deployment pipelines.",[27,82346,82347],{},"However, since the SDK is in technical preview, getting authentication working in CI proved challenging. The Copilot CLI typically requires interactive authentication, which doesn't play nicely with automated environments. It might become easier as the SDK matures, but for now, running the script locally before pushing works perfectly fine.",[104,82349,82351],{"id":82350},"looking-ahead","Looking Ahead",[27,82353,82354],{},"This is a fairly simple use case, but it demonstrates the potential of programmatic access to Copilot. Instead of just helping you write code, it can now be part of your toolchain - processing files, transforming content, and automating workflows that previously required manual intervention.",[27,82356,82357],{},"I'm already thinking about what else I could automate. What if the script could read articles directly from Notion, where I do my initial drafting? What if it could generate social media snippets automatically? The possibilities are genuinely exciting.",[27,82359,82360],{},"The Copilot SDK is still early days, but for those of us who like to experiment with new tools, it's already proving useful. If you have repetitive tasks that involve processing text or files with some intelligence required, it might be worth giving it a try.",[27,82362,82363,82364,82368,82369,82376],{},"You can find the official SDK repository and documentation at ",[45,82365,82367],{"href":80518,"target":2716,"rel":82366},[2718,2719],"github.com\u002Fgithub\u002Fcopilot-sdk",". And if you want to see the complete conversion script I built, check out the ",[45,82370,82373],{"href":82371,"target":2716,"rel":82372},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fpersonal-blog-2023\u002Fblob\u002Fmain\u002Fscripts\u002Fconvert-article.ts",[2718,2719],[22,82374,82375],{},"scripts\u002Fconvert-article.ts"," file in my blog's repository.",[27,82378,82379],{},"Happy automating! 🚀",[2617,82381,82382],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":133,"searchDepth":173,"depth":173,"links":82384},[82385,82386,82387,82388,82389,82397,82398,82399],{"id":80456,"depth":173,"text":80457},{"id":80499,"depth":173,"text":80500},{"id":80512,"depth":173,"text":80513},{"id":80586,"depth":173,"text":80587},{"id":80656,"depth":173,"text":80657,"children":82390},[82391,82392,82393,82394,82395,82396],{"id":47891,"depth":188,"text":47892},{"id":80791,"depth":188,"text":80792},{"id":80857,"depth":188,"text":80858},{"id":81022,"depth":188,"text":81023},{"id":81364,"depth":188,"text":81365},{"id":82052,"depth":188,"text":82053},{"id":82300,"depth":173,"text":82301},{"id":82340,"depth":173,"text":82341},{"id":82350,"depth":173,"text":82351},"I built a TypeScript script using the GitHub Copilot SDK to automate my blog's publishing workflow. Here's how it converts Markdown drafts into fully structured blog posts with proper frontmatter, SEO metadata, image handling, and more.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1770033517\u002Fblog\u002Fautomating-my-blog-workflow-with-github-copilot-sdk\u002Fautomating-my-blog-workflow-with-github-copilot-sdk_atnuqy",[80520,82403,82404,82405,80602,72591,82406,82407,82408,82409,82410,82411,82412,82413,82414,82415,82416,82417,82418,80391],"blog automation","Markdown conversion","TypeScript automation","file attachments","SEO metadata","frontmatter generation","Node.js scripting","developer workflow","content automation","static site generator","Cloudinary images","blog publishing","agentic engine","multi-turn conversations","model selection","programmatic API",{},"\u002F2026\u002F02\u002F08\u002Fautomating-my-blog-workflow-with-github-copilot-sdk","8th February 2026",{"title":80412,"description":82400},"2026\u002F02\u002F08\u002Fautomating-my-blog-workflow-with-github-copilot-sdk","1BX_VWVneOlde3gbVBLcnNQllU9dl96I34lcMNHoW4U",{"id":82426,"title":82427,"articleTags":82428,"author":11,"blog":12,"body":82429,"description":84443,"extension":2649,"image":84444,"keywords":84445,"meta":84460,"navigation":515,"path":84461,"published":84462,"readTime":667,"seo":84463,"stem":84464,"type":2662,"__hash__":84465},"content\u002F2026\u002F02\u002F15\u002Fthe-protocol-behind-bluesky-rethinking-social-media-architecture.md","The Protocol Behind Bluesky: Rethinking Social Media Architecture",[52208,2669,12817],{"type":14,"value":82430,"toc":84420},[82431,82434,82448,82450,82454,82459,82462,82471,82474,82478,82485,82519,82523,82526,82540,82544,82551,82558,82565,82572,82576,82579,82585,82599,82610,82614,82626,82636,82650,82656,82660,82663,82706,82710,82718,82720,82772,82788,82842,82847,82853,82857,82863,82885,82891,82895,83125,83135,83290,83295,83299,83302,83572,83587,83591,83597,83700,83714,83718,83730,83739,83743,83750,83754,83761,83764,83768,83771,83786,83789,84050,84060,84067,84071,84074,84082,84091,84230,84236,84246,84307,84317,84323,84329,84333,84336,84394,84397,84400,84402,84405,84408,84411,84417],[17,82432,82427],{"id":82433},"the-protocol-behind-bluesky-rethinking-social-media-architecture",[27,82435,82436],{},[30,82437,82438,36,82440,40,82442],{},[33,82439],{"value":35},[33,82441],{"value":39},[42,82443,82444],{},[45,82445,82446],{"href":47},[33,82447],{"value":50},[52,82449],{":tags":54},[56,82451],{":audio-src":82452,":transcript-src":82453},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F02\u002F15\u002Fthe-protocol-behind-bluesky-rethinking-social-media-architecture\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F02\u002F15\u002Fthe-protocol-behind-bluesky-rethinking-social-media-architecture\u002Fsummary.json",[27,82455,82456],{},[63,82457],{"alt":12847,"src":82458},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1770893361\u002Fblog\u002Fthe-protocol-behind-bluesky-rethinking-social-media-architecture\u002Fthe-protocol-behind-bluesky-rethinking-social-media-architecture-2_bqif19",[27,82460,82461],{},"What if you could take your entire social media presence - your posts, your followers, your identity - and move it to a different platform the same way you'd switch email providers? No data loss, no starting over, no asking for permission.",[27,82463,82464,82465,82470],{},"That's the promise behind the AT Protocol. And it's not just a whitepaper idea - it's the protocol running under ",[45,82466,82469],{"href":82467,"target":2716,"rel":82468},"https:\u002F\u002Fbsky.app\u002F",[2718,2719],"Bluesky"," right now, with real users and real data.",[27,82472,82473],{},"I've been spending time with it recently, both reading about the architecture and building against its APIs. In this article, I want to share what I've learned: what this protocol actually is, how it compares to things you might confuse it with, and what it looks like to build a simple app on top of it.",[104,82475,82477],{"id":82476},"what-is-the-at-protocol","What Is the AT Protocol?",[27,82479,82480,82481,82484],{},"The AT Protocol - short for ",[42,82482,82483],{},"Authenticated Transfer Protocol"," - is an open protocol for decentralised social networking. It's the technical foundation behind Bluesky, but it's designed to be much bigger than a single app. The goal is to give users real ownership over their data, their identity, and their social graph - all portable and not locked into any one platform.",[27,82486,82487,82488,82491,82492,82497,82498,164,82501,82506,82507,82510,82511,82514,82515,82518],{},"The project started in ",[42,82489,82490],{},"2019"," inside Twitter, when then-CEO ",[45,82493,82496],{"href":82494,"target":2716,"rel":82495},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FJack_Dorsey",[2718,2719],"Jack Dorsey"," initiated an effort to explore open standards for social media. In ",[42,82499,82500],{},"2021",[45,82502,82505],{"href":82503,"target":2716,"rel":82504},"https:\u002F\u002Fbsky.app\u002Fprofile\u002Fjay.bsky.team",[2718,2719],"Jay Graber"," was hired to lead the project and incorporated it as an independent company - ",[42,82508,82509],{},"Bluesky Social PBC"," - separate from Twitter. The early prototype was called \"ADX\" (Authenticated Data Experiment). After a summer of iteration, the protocol was formally announced as the AT Protocol in ",[42,82512,82513],{},"October 2022",". As of early 2026, its core specifications are being standardised within the ",[42,82516,82517],{},"IETF"," (Internet Engineering Task Force).",[104,82520,82522],{"id":82521},"what-problem-does-it-try-to-solve","What Problem Does It Try to Solve?",[27,82524,82525],{},"The current social media model is straightforward: a company owns the platform, and the platform owns your data. If they change the rules, the algorithm, or the policies - you either accept it or you leave. And if you leave, you lose everything.",[27,82527,82528,82529,114,82532,82535,82536,82539],{},"The AT Protocol tries to break this by separating ",[42,82530,82531],{},"your identity",[42,82533,82534],{},"your data"," from any single service. Your account lives on a ",[42,82537,82538],{},"Personal Data Server (PDS)",", and if you don't like the one you're on, you can move to another - or host your own - and bring everything with you. Your handle, your posts, your followers. Nothing is lost.",[104,82541,82543],{"id":82542},"think-of-it-like-company-email","Think of It Like Company Email",[27,82545,82546,82547,82550],{},"To make this concrete, think about how ",[42,82548,82549],{},"company email"," works.",[27,82552,82553,82554,82557],{},"Your company runs an email server. All your emails live on that server. But email works on an open protocol - ",[42,82555,82556],{},"SMTP",". If your company decides to switch email providers, they migrate the data to the new server. Same protocol, different server. You keep your emails, your contacts, everything.",[27,82559,82560,82561,82564],{},"Now imagine the same thing for social media. Your posts live on a server (your PDS). The protocol connecting everything is the AT Protocol. If you want to move to a different server - maybe for better performance, different moderation policies, or because you want to self-host - you take your data and move. Same protocol, different server. Your identity stays the same because it's tied to a ",[42,82562,82563],{},"DID (Decentralised Identifier)",", not to the server itself.",[3244,82566,82567],{},[27,82568,82569],{},[42,82570,82571],{},"Your data is yours, and the protocol is the common language that makes everything work together.",[104,82573,82575],{"id":82574},"how-is-this-different-from-blockchain","How Is This Different from Blockchain?",[27,82577,82578],{},"When people hear \"decentralised,\" they immediately think of crypto and blockchain. The terminology overlaps, so the confusion is understandable. But these are fundamentally different approaches.",[27,82580,82581,82584],{},[42,82582,82583],{},"Blockchain platforms"," like Ethereum rely on consensus mechanisms (Proof of Work, Proof of Stake) where every node agrees on a shared ledger. Great for financial transactions where you need trustless verification - but it comes with high latency, energy costs, and scaling challenges. Posting a social media update doesn't need the same guarantees as transferring money.",[27,82586,82587,82590,82591,82594,82595,82598],{},[42,82588,82589],{},"The AT Protocol"," uses a ",[42,82592,82593],{},"federated model",". Your data lives on servers (PDS instances), not across a peer-to-peer network. Instead of blockchain consensus, it uses ",[42,82596,82597],{},"cryptographic signatures"," on your data repository. Your content is signed with your keys, verifiable as authentic without needing a blockchain to prove it.",[27,82600,82601,82602,82605,82606,82609],{},"Blockchain decentralises ",[42,82603,82604],{},"trust"," through consensus. The AT Protocol decentralises ",[42,82607,82608],{},"data ownership"," through signed repositories and portable identities. They sound similar, but they're solving different problems.",[104,82611,82613],{"id":82612},"how-is-this-different-from-mastodon","How Is This Different from Mastodon?",[27,82615,82616,82617,114,82622,82625],{},"If you've explored decentralised social media, you've probably come across ",[45,82618,82621],{"href":82619,"target":2716,"rel":82620},"https:\u002F\u002Fmastodon.social\u002F",[2718,2719],"Mastodon",[42,82623,82624],{},"ActivityPub",". Both aim for decentralisation. Both are open protocols. But they differ in key ways.",[27,82627,82628,82631,82632,82635],{},[42,82629,82630],{},"Account portability"," is the big one. In Mastodon, your account lives on the instance you signed up on. If that instance shuts down, your data is gone. Migration requires cooperation from the original server, and old posts don't come with you. In the AT Protocol, your data is stored in a ",[42,82633,82634],{},"signed repository"," tied to your DID. You can move everything to a new PDS without the old server's involvement.",[27,82637,82638,82641,82642,82645,82646,82649],{},[42,82639,82640],{},"Scalability"," is another distinction. ActivityPub uses per-server inboxes and outboxes - servers message each other directly. Popular instances get flooded with traffic. The AT Protocol uses ",[42,82643,82644],{},"Relays"," that aggregate content into a unified stream, and ",[42,82647,82648],{},"AppViews"," that index and serve data to applications. This makes global-scale networking more practical.",[27,82651,82652,82655],{},[42,82653,82654],{},"Discoverability"," follows from this. Finding content across Mastodon instances can be hit-or-miss. The AT Protocol's centralised indexing through AppViews makes global search straightforward.",[104,82657,82659],{"id":82658},"how-it-works-behind-the-scenes","How It Works Behind the Scenes",[27,82661,82662],{},"When you use Bluesky, here's what's actually happening:",[2569,82664,82665,82675,82685,82695,82700],{},[1006,82666,82667,82670,82671,82674],{},[42,82668,82669],{},"Your identity"," is a DID - a permanent identifier that belongs to you. Your handle (",[22,82672,82673],{},"alice.bsky.social",") is a friendly alias pointing to it.",[1006,82676,82677,82680,82681,82684],{},[42,82678,82679],{},"Your data"," - posts, likes, follows - lives in your ",[42,82682,82683],{},"PDS",". Bluesky hosts one for you by default, but you could run your own.",[1006,82686,82687,82690,82691,82694],{},[42,82688,82689],{},"When you post",", the data is written to your PDS. Your PDS emits the event to a ",[42,82692,82693],{},"Relay",", which aggregates updates from all PDS instances into a real-time stream (the \"firehose\").",[1006,82696,82697,82699],{},[42,82698,82648],{}," subscribe to this stream, index the data, and present it to users. Think of an AppView as a search engine for the network.",[1006,82701,82702,82705],{},[42,82703,82704],{},"Anyone can build an AppView."," Different algorithms, different moderation, different interface - all reading from the same underlying data.",[104,82707,82709],{"id":82708},"building-a-simple-at-protocol-app","Building a Simple AT Protocol App",[27,82711,82712,82713,82717],{},"Let's build a Node.js web app that authenticates users through AT Protocol OAuth and lets them view their profile and create posts. The full source code is in the ",[45,82714,71386],{"href":82715,"target":2716,"rel":82716},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002FAT-Protocol-Authenticated-Transfer-Protocol---OAuth-Demo-App",[2718,2719]," - here I'll focus on the parts that matter.",[123,82719,47892],{"id":47891},[128,82721,82723],{"className":8665,"code":82722,"language":8667,"meta":133,"style":133},"npm init -y\nnpm install @atproto\u002Fapi @atproto\u002Foauth-client-node express iron-session better-sqlite3 kysely\nnpm install -D tsx @types\u002Fexpress @types\u002Fbetter-sqlite3\n",[22,82724,82725,82733,82756],{"__ignoreMap":133},[137,82726,82727,82729,82731],{"class":139,"line":140},[137,82728,9536],{"class":147},[137,82730,9539],{"class":284},[137,82732,57291],{"class":364},[137,82734,82735,82737,82739,82742,82745,82747,82750,82753],{"class":139,"line":173},[137,82736,9536],{"class":147},[137,82738,10268],{"class":284},[137,82740,82741],{"class":284}," @atproto\u002Fapi",[137,82743,82744],{"class":284}," @atproto\u002Foauth-client-node",[137,82746,67932],{"class":284},[137,82748,82749],{"class":284}," iron-session",[137,82751,82752],{"class":284}," better-sqlite3",[137,82754,82755],{"class":284}," kysely\n",[137,82757,82758,82760,82762,82764,82766,82769],{"class":139,"line":188},[137,82759,9536],{"class":147},[137,82761,10268],{"class":284},[137,82763,23060],{"class":364},[137,82765,49162],{"class":284},[137,82767,82768],{"class":284}," @types\u002Fexpress",[137,82770,82771],{"class":284}," @types\u002Fbetter-sqlite3\n",[27,82773,82774,82775,82779,82780,82783,82784,82787],{},"We're using ",[45,82776,29200],{"href":82777,"target":2716,"rel":82778},"https:\u002F\u002Ftsx.is\u002F",[2718,2719]," to run TypeScript directly - no build step. The ",[22,82781,82782],{},"--env-file"," flag loads environment variables natively, so no ",[22,82785,82786],{},"dotenv"," needed either.",[128,82789,82791],{"className":5155,"code":82790,"language":5157,"meta":133,"style":133},"{\n    \"type\": \"module\",\n    \"scripts\": {\n        \"start\": \"tsx --env-file=.env src\u002Findex.ts\",\n        \"dev\": \"tsx watch --env-file=.env src\u002Findex.ts\"\n    }\n}\n",[22,82792,82793,82797,82807,82813,82824,82834,82838],{"__ignoreMap":133},[137,82794,82795],{"class":139,"line":140},[137,82796,15971],{"class":157},[137,82798,82799,82801,82803,82805],{"class":139,"line":173},[137,82800,57339],{"class":364},[137,82802,726],{"class":157},[137,82804,25777],{"class":284},[137,82806,1961],{"class":157},[137,82808,82809,82811],{"class":139,"line":188},[137,82810,57350],{"class":364},[137,82812,1819],{"class":157},[137,82814,82815,82817,82819,82822],{"class":139,"line":269},[137,82816,57357],{"class":364},[137,82818,726],{"class":157},[137,82820,82821],{"class":284},"\"tsx --env-file=.env src\u002Findex.ts\"",[137,82823,1961],{"class":157},[137,82825,82826,82829,82831],{"class":139,"line":278},[137,82827,82828],{"class":364},"        \"dev\"",[137,82830,726],{"class":157},[137,82832,82833],{"class":284},"\"tsx watch --env-file=.env src\u002Findex.ts\"\n",[137,82835,82836],{"class":139,"line":291},[137,82837,294],{"class":157},[137,82839,82840],{"class":139,"line":297},[137,82841,510],{"class":157},[27,82843,4737,82844,82846],{},[22,82845,13489],{}," file just needs two values:",[128,82848,82851],{"className":82849,"code":82850,"language":5189},[5187],"PORT=3000\nCOOKIE_SECRET=super-secret-cookie-key-for-development\n",[22,82852,82850],{"__ignoreMap":133},[123,82854,82856],{"id":82855},"the-project-structure","The Project Structure",[128,82858,82861],{"className":82859,"code":82860,"language":5189},[5187],"src\u002F\n├── db.ts      # SQLite database for OAuth state\u002Fsessions\n├── auth.ts    # OAuth client setup and storage stores\n└── index.ts   # Express server with all routes\n",[22,82862,82860],{"__ignoreMap":133},[27,82864,82865,82866,82869,82870,114,82873,82876,82877,82880,82881,82884],{},"The OAuth client needs persistent storage for session data and flow state. ",[22,82867,82868],{},"src\u002Fdb.ts"," sets up an in-memory SQLite database with two simple key-value tables (",[22,82871,82872],{},"auth_state",[22,82874,82875],{},"auth_session",") using Kysely as a type-safe query builder. ",[22,82878,82879],{},"src\u002Fauth.ts"," implements the storage interfaces the OAuth client expects and creates the client in ",[42,82882,82883],{},"loopback mode"," - a special development mode that works on localhost without needing a public URL or private keys.",[27,82886,82887,82888,1017],{},"The interesting part is in ",[22,82889,82890],{},"src\u002Findex.ts",[123,82892,82894],{"id":82893},"bootstrapping-the-server","Bootstrapping the Server",[128,82896,82898],{"className":13299,"code":82897,"language":13301,"meta":133,"style":133},"import { Agent } from \"@atproto\u002Fapi\";\nimport express from \"express\";\nimport { getIronSession } from \"iron-session\";\nimport { createDb, migrateToLatest } from \".\u002Fdb.js\";\nimport { createOAuthClient } from \".\u002Fauth.js\";\n\ntype Session = { did?: string };\n\nasync function main() {\n    const db = createDb(\":memory:\");\n    await migrateToLatest(db);\n    const oauthClient = await createOAuthClient(db);\n\n    const app = express();\n    app.use(express.urlencoded({ extended: true }));\n\n    \u002F\u002F ... routes go here\n\n    app.listen(Number(process.env.PORT) || 3000);\n}\n",[22,82899,82900,82914,82927,82941,82955,82969,82973,82993,82997,83007,83025,83034,83050,83054,83066,83085,83089,83094,83098,83121],{"__ignoreMap":133},[137,82901,82902,82904,82907,82909,82912],{"class":139,"line":140},[137,82903,10287],{"class":143},[137,82905,82906],{"class":157}," { Agent } ",[137,82908,10954],{"class":143},[137,82910,82911],{"class":284}," \"@atproto\u002Fapi\"",[137,82913,3276],{"class":157},[137,82915,82916,82918,82921,82923,82925],{"class":139,"line":173},[137,82917,10287],{"class":143},[137,82919,82920],{"class":157}," express ",[137,82922,10954],{"class":143},[137,82924,67733],{"class":284},[137,82926,3276],{"class":157},[137,82928,82929,82931,82934,82936,82939],{"class":139,"line":188},[137,82930,10287],{"class":143},[137,82932,82933],{"class":157}," { getIronSession } ",[137,82935,10954],{"class":143},[137,82937,82938],{"class":284}," \"iron-session\"",[137,82940,3276],{"class":157},[137,82942,82943,82945,82948,82950,82953],{"class":139,"line":269},[137,82944,10287],{"class":143},[137,82946,82947],{"class":157}," { createDb, migrateToLatest } ",[137,82949,10954],{"class":143},[137,82951,82952],{"class":284}," \".\u002Fdb.js\"",[137,82954,3276],{"class":157},[137,82956,82957,82959,82962,82964,82967],{"class":139,"line":278},[137,82958,10287],{"class":143},[137,82960,82961],{"class":157}," { createOAuthClient } ",[137,82963,10954],{"class":143},[137,82965,82966],{"class":284}," \".\u002Fauth.js\"",[137,82968,3276],{"class":157},[137,82970,82971],{"class":139,"line":291},[137,82972,516],{"emptyLinePlaceholder":515},[137,82974,82975,82977,82980,82982,82984,82987,82989,82991],{"class":139,"line":297},[137,82976,20355],{"class":143},[137,82978,82979],{"class":147}," Session",[137,82981,151],{"class":143},[137,82983,8906],{"class":157},[137,82985,82986],{"class":161},"did",[137,82988,35824],{"class":143},[137,82990,13630],{"class":364},[137,82992,32107],{"class":157},[137,82994,82995],{"class":139,"line":302},[137,82996,516],{"emptyLinePlaceholder":515},[137,82998,82999,83001,83003,83005],{"class":139,"line":662},[137,83000,15050],{"class":143},[137,83002,154],{"class":143},[137,83004,58463],{"class":147},[137,83006,275],{"class":157},[137,83008,83009,83011,83013,83015,83018,83020,83023],{"class":139,"line":667},[137,83010,4177],{"class":143},[137,83012,48654],{"class":364},[137,83014,151],{"class":143},[137,83016,83017],{"class":147}," createDb",[137,83019,356],{"class":157},[137,83021,83022],{"class":284},"\":memory:\"",[137,83024,1502],{"class":157},[137,83026,83027,83029,83032],{"class":139,"line":786},[137,83028,15100],{"class":143},[137,83030,83031],{"class":147}," migrateToLatest",[137,83033,48679],{"class":157},[137,83035,83036,83038,83041,83043,83045,83048],{"class":139,"line":798},[137,83037,4177],{"class":143},[137,83039,83040],{"class":364}," oauthClient",[137,83042,151],{"class":143},[137,83044,15069],{"class":143},[137,83046,83047],{"class":147}," createOAuthClient",[137,83049,48679],{"class":157},[137,83051,83052],{"class":139,"line":803},[137,83053,516],{"emptyLinePlaceholder":515},[137,83055,83056,83058,83060,83062,83064],{"class":139,"line":931},[137,83057,4177],{"class":143},[137,83059,15064],{"class":364},[137,83061,151],{"class":143},[137,83063,67932],{"class":147},[137,83065,924],{"class":157},[137,83067,83068,83070,83072,83074,83077,83080,83082],{"class":139,"line":1568},[137,83069,15083],{"class":157},[137,83071,67941],{"class":147},[137,83073,67957],{"class":157},[137,83075,83076],{"class":147},"urlencoded",[137,83078,83079],{"class":157},"({ extended: ",[137,83081,3097],{"class":364},[137,83083,83084],{"class":157}," }));\n",[137,83086,83087],{"class":139,"line":1573},[137,83088,516],{"emptyLinePlaceholder":515},[137,83090,83091],{"class":139,"line":1578},[137,83092,83093],{"class":308},"    \u002F\u002F ... routes go here\n",[137,83095,83096],{"class":139,"line":1588},[137,83097,516],{"emptyLinePlaceholder":515},[137,83099,83100,83102,83104,83106,83108,83110,83112,83114,83116,83119],{"class":139,"line":1601},[137,83101,15083],{"class":157},[137,83103,15106],{"class":147},[137,83105,356],{"class":157},[137,83107,45027],{"class":147},[137,83109,67859],{"class":157},[137,83111,68764],{"class":364},[137,83113,219],{"class":157},[137,83115,50706],{"class":143},[137,83117,83118],{"class":364}," 3000",[137,83120,1502],{"class":157},[137,83122,83123],{"class":139,"line":3802},[137,83124,510],{"class":157},[27,83126,83127,83128,83131,83132,894],{},"On startup, we create the database, run migrations, and initialise the OAuth client. The session cookie stores just one thing - the user's ",[42,83129,83130],{},"DID",". From that, we can restore the full OAuth session and get an authenticated ",[22,83133,83134],{},"Agent",[128,83136,83138],{"className":13299,"code":83137,"language":13301,"meta":133,"style":133},"async function getSessionAgent(req: express.Request, res: express.Response) {\n    const session = await getIronSession\u003CSession>(req, res, {\n        cookieName: \"sid\",\n        password: process.env.COOKIE_SECRET!,\n    });\n    if (!session.did) return null;\n\n    const oauthSession = await oauthClient.restore(session.did);\n    return oauthSession ? new Agent(oauthSession) : null;\n}\n",[22,83139,83140,83176,83197,83207,83219,83223,83240,83244,83264,83286],{"__ignoreMap":133},[137,83141,83142,83144,83146,83149,83151,83153,83155,83157,83159,83161,83163,83165,83167,83169,83171,83174],{"class":139,"line":140},[137,83143,15050],{"class":143},[137,83145,154],{"class":143},[137,83147,83148],{"class":147}," getSessionAgent",[137,83150,356],{"class":157},[137,83152,9133],{"class":161},[137,83154,894],{"class":143},[137,83156,67932],{"class":147},[137,83158,1017],{"class":157},[137,83160,22695],{"class":147},[137,83162,164],{"class":157},[137,83164,9138],{"class":161},[137,83166,894],{"class":143},[137,83168,67932],{"class":147},[137,83170,1017],{"class":157},[137,83172,83173],{"class":147},"Response",[137,83175,170],{"class":157},[137,83177,83178,83180,83182,83184,83186,83189,83191,83194],{"class":139,"line":173},[137,83179,4177],{"class":143},[137,83181,81479],{"class":364},[137,83183,151],{"class":143},[137,83185,15069],{"class":143},[137,83187,83188],{"class":147}," getIronSession",[137,83190,4033],{"class":157},[137,83192,83193],{"class":147},"Session",[137,83195,83196],{"class":157},">(req, res, {\n",[137,83198,83199,83202,83205],{"class":139,"line":188},[137,83200,83201],{"class":157},"        cookieName: ",[137,83203,83204],{"class":284},"\"sid\"",[137,83206,1961],{"class":157},[137,83208,83209,83212,83215,83217],{"class":139,"line":269},[137,83210,83211],{"class":157},"        password: process.env.",[137,83213,83214],{"class":364},"COOKIE_SECRET",[137,83216,17393],{"class":143},[137,83218,1961],{"class":157},[137,83220,83221],{"class":139,"line":278},[137,83222,2832],{"class":157},[137,83224,83225,83227,83229,83231,83234,83236,83238],{"class":139,"line":291},[137,83226,24696],{"class":143},[137,83228,158],{"class":157},[137,83230,17393],{"class":143},[137,83232,83233],{"class":157},"session.did) ",[137,83235,5428],{"class":143},[137,83237,3417],{"class":364},[137,83239,3276],{"class":157},[137,83241,83242],{"class":139,"line":297},[137,83243,516],{"emptyLinePlaceholder":515},[137,83245,83246,83248,83251,83253,83255,83258,83261],{"class":139,"line":302},[137,83247,4177],{"class":143},[137,83249,83250],{"class":364}," oauthSession",[137,83252,151],{"class":143},[137,83254,15069],{"class":143},[137,83256,83257],{"class":157}," oauthClient.",[137,83259,83260],{"class":147},"restore",[137,83262,83263],{"class":157},"(session.did);\n",[137,83265,83266,83268,83271,83273,83275,83277,83280,83282,83284],{"class":139,"line":662},[137,83267,176],{"class":143},[137,83269,83270],{"class":157}," oauthSession ",[137,83272,12972],{"class":143},[137,83274,1426],{"class":143},[137,83276,57420],{"class":147},[137,83278,83279],{"class":157},"(oauthSession) ",[137,83281,894],{"class":143},[137,83283,3417],{"class":364},[137,83285,3276],{"class":157},[137,83287,83288],{"class":139,"line":667},[137,83289,510],{"class":157},[27,83291,4737,83292,83294],{},[22,83293,83134],{}," is the AT Protocol client - once you have one, you can read profiles, create posts, and interact with any data on the network.",[123,83296,83298],{"id":83297},"the-oauth-login-flow","The OAuth Login Flow",[27,83300,83301],{},"The login has three parts: a form, the redirect, and the callback.",[128,83303,83305],{"className":13299,"code":83304,"language":13301,"meta":133,"style":133},"\u002F\u002F User submits their handle → we start the OAuth flow\napp.post(\"\u002Flogin\", async (req, res) => {\n    const url = await oauthClient.authorize(req.body.handle, {\n        scope: \"atproto transition:generic\",\n    });\n    res.redirect(url.toString());\n});\n\n\u002F\u002F Bluesky redirects back → we complete the handshake\napp.get(\"\u002Foauth\u002Fcallback\", async (req, res) => {\n    const params = new URLSearchParams(req.originalUrl.split(\"?\")[1]);\n    const { session: oauthSession } = await oauthClient.callback(params);\n\n    const session = await getIronSession\u003CSession>(req, res, {\n        cookieName: \"sid\",\n        password: process.env.COOKIE_SECRET!,\n    });\n    session.did = oauthSession.did;\n    await session.save();\n\n    res.redirect(\"\u002F\");\n});\n",[22,83306,83307,83312,83341,83360,83370,83374,83388,83392,83396,83401,83430,83460,83487,83491,83509,83517,83527,83531,83541,83551,83555,83568],{"__ignoreMap":133},[137,83308,83309],{"class":139,"line":140},[137,83310,83311],{"class":308},"\u002F\u002F User submits their handle → we start the OAuth flow\n",[137,83313,83314,83316,83318,83320,83323,83325,83327,83329,83331,83333,83335,83337,83339],{"class":139,"line":173},[137,83315,21014],{"class":157},[137,83317,12],{"class":147},[137,83319,356],{"class":157},[137,83321,83322],{"class":284},"\"\u002Flogin\"",[137,83324,164],{"class":157},[137,83326,15050],{"class":143},[137,83328,158],{"class":157},[137,83330,9133],{"class":161},[137,83332,164],{"class":157},[137,83334,9138],{"class":161},[137,83336,219],{"class":157},[137,83338,222],{"class":143},[137,83340,256],{"class":157},[137,83342,83343,83345,83348,83350,83352,83354,83357],{"class":139,"line":188},[137,83344,4177],{"class":143},[137,83346,83347],{"class":364}," url",[137,83349,151],{"class":143},[137,83351,15069],{"class":143},[137,83353,83257],{"class":157},[137,83355,83356],{"class":147},"authorize",[137,83358,83359],{"class":157},"(req.body.handle, {\n",[137,83361,83362,83365,83368],{"class":139,"line":269},[137,83363,83364],{"class":157},"        scope: ",[137,83366,83367],{"class":284},"\"atproto transition:generic\"",[137,83369,1961],{"class":157},[137,83371,83372],{"class":139,"line":278},[137,83373,2832],{"class":157},[137,83375,83376,83378,83381,83384,83386],{"class":139,"line":291},[137,83377,9163],{"class":157},[137,83379,83380],{"class":147},"redirect",[137,83382,83383],{"class":157},"(url.",[137,83385,7692],{"class":147},[137,83387,14173],{"class":157},[137,83389,83390],{"class":139,"line":297},[137,83391,5422],{"class":157},[137,83393,83394],{"class":139,"line":302},[137,83395,516],{"emptyLinePlaceholder":515},[137,83397,83398],{"class":139,"line":662},[137,83399,83400],{"class":308},"\u002F\u002F Bluesky redirects back → we complete the handshake\n",[137,83402,83403,83405,83407,83409,83412,83414,83416,83418,83420,83422,83424,83426,83428],{"class":139,"line":667},[137,83404,21014],{"class":157},[137,83406,14153],{"class":147},[137,83408,356],{"class":157},[137,83410,83411],{"class":284},"\"\u002Foauth\u002Fcallback\"",[137,83413,164],{"class":157},[137,83415,15050],{"class":143},[137,83417,158],{"class":157},[137,83419,9133],{"class":161},[137,83421,164],{"class":157},[137,83423,9138],{"class":161},[137,83425,219],{"class":157},[137,83427,222],{"class":143},[137,83429,256],{"class":157},[137,83431,83432,83434,83437,83439,83441,83444,83447,83449,83451,83454,83456,83458],{"class":139,"line":786},[137,83433,4177],{"class":143},[137,83435,83436],{"class":364}," params",[137,83438,151],{"class":143},[137,83440,1426],{"class":143},[137,83442,83443],{"class":147}," URLSearchParams",[137,83445,83446],{"class":157},"(req.originalUrl.",[137,83448,8537],{"class":147},[137,83450,356],{"class":157},[137,83452,83453],{"class":284},"\"?\"",[137,83455,14224],{"class":157},[137,83457,6065],{"class":364},[137,83459,43556],{"class":157},[137,83461,83462,83464,83466,83469,83471,83474,83476,83478,83480,83482,83484],{"class":139,"line":798},[137,83463,4177],{"class":143},[137,83465,8906],{"class":157},[137,83467,83468],{"class":161},"session",[137,83470,726],{"class":157},[137,83472,83473],{"class":364},"oauthSession",[137,83475,8911],{"class":157},[137,83477,253],{"class":143},[137,83479,15069],{"class":143},[137,83481,83257],{"class":157},[137,83483,35264],{"class":147},[137,83485,83486],{"class":157},"(params);\n",[137,83488,83489],{"class":139,"line":803},[137,83490,516],{"emptyLinePlaceholder":515},[137,83492,83493,83495,83497,83499,83501,83503,83505,83507],{"class":139,"line":931},[137,83494,4177],{"class":143},[137,83496,81479],{"class":364},[137,83498,151],{"class":143},[137,83500,15069],{"class":143},[137,83502,83188],{"class":147},[137,83504,4033],{"class":157},[137,83506,83193],{"class":147},[137,83508,83196],{"class":157},[137,83510,83511,83513,83515],{"class":139,"line":1568},[137,83512,83201],{"class":157},[137,83514,83204],{"class":284},[137,83516,1961],{"class":157},[137,83518,83519,83521,83523,83525],{"class":139,"line":1573},[137,83520,83211],{"class":157},[137,83522,83214],{"class":364},[137,83524,17393],{"class":143},[137,83526,1961],{"class":157},[137,83528,83529],{"class":139,"line":1578},[137,83530,2832],{"class":157},[137,83532,83533,83536,83538],{"class":139,"line":1588},[137,83534,83535],{"class":157},"    session.did ",[137,83537,253],{"class":143},[137,83539,83540],{"class":157}," oauthSession.did;\n",[137,83542,83543,83545,83547,83549],{"class":139,"line":1601},[137,83544,15100],{"class":143},[137,83546,81539],{"class":157},[137,83548,32086],{"class":147},[137,83550,924],{"class":157},[137,83552,83553],{"class":139,"line":3802},[137,83554,516],{"emptyLinePlaceholder":515},[137,83556,83557,83559,83561,83563,83566],{"class":139,"line":3808},[137,83558,9163],{"class":157},[137,83560,83380],{"class":147},[137,83562,356],{"class":157},[137,83564,83565],{"class":284},"\"\u002F\"",[137,83567,1502],{"class":157},[137,83569,83570],{"class":139,"line":3822},[137,83571,5422],{"class":157},[27,83573,83574,83575,83578,83579,83582,83583,83586],{},"When the user submits their handle, ",[22,83576,83577],{},"oauthClient.authorize()"," resolves which server hosts their account and generates an authorization URL. The user gets redirected to ",[42,83580,83581],{},"their own Bluesky server",", approves the request, and comes back to ",[22,83584,83585],{},"\u002Foauth\u002Fcallback",". We complete the handshake and store their DID. We never see their password.",[123,83588,83590],{"id":83589},"reading-and-writing-data","Reading and Writing Data",[27,83592,83593,83594,83596],{},"Once authenticated, the ",[22,83595,83134],{}," can do everything:",[128,83598,83600],{"className":13299,"code":83599,"language":13301,"meta":133,"style":133},"\u002F\u002F Fetch the user's profile\nconst { data: profile } = await agent.getProfile({ actor: agent.assertDid });\n\n\u002F\u002F Fetch their recent posts\nconst { data: feed } = await agent.getAuthorFeed({ actor: agent.assertDid, limit: 5 });\n\n\u002F\u002F Create a new post\nawait agent.post({ text: \"Hello from my AT Protocol app!\" });\n",[22,83601,83602,83607,83635,83639,83644,83675,83679,83684],{"__ignoreMap":133},[137,83603,83604],{"class":139,"line":140},[137,83605,83606],{"class":308},"\u002F\u002F Fetch the user's profile\n",[137,83608,83609,83611,83613,83615,83617,83620,83622,83624,83626,83629,83632],{"class":139,"line":173},[137,83610,3077],{"class":143},[137,83612,8906],{"class":157},[137,83614,1942],{"class":161},[137,83616,726],{"class":157},[137,83618,83619],{"class":364},"profile",[137,83621,8911],{"class":157},[137,83623,253],{"class":143},[137,83625,15069],{"class":143},[137,83627,83628],{"class":157}," agent.",[137,83630,83631],{"class":147},"getProfile",[137,83633,83634],{"class":157},"({ actor: agent.assertDid });\n",[137,83636,83637],{"class":139,"line":188},[137,83638,516],{"emptyLinePlaceholder":515},[137,83640,83641],{"class":139,"line":269},[137,83642,83643],{"class":308},"\u002F\u002F Fetch their recent posts\n",[137,83645,83646,83648,83650,83652,83654,83657,83659,83661,83663,83665,83668,83671,83673],{"class":139,"line":278},[137,83647,3077],{"class":143},[137,83649,8906],{"class":157},[137,83651,1942],{"class":161},[137,83653,726],{"class":157},[137,83655,83656],{"class":364},"feed",[137,83658,8911],{"class":157},[137,83660,253],{"class":143},[137,83662,15069],{"class":143},[137,83664,83628],{"class":157},[137,83666,83667],{"class":147},"getAuthorFeed",[137,83669,83670],{"class":157},"({ actor: agent.assertDid, limit: ",[137,83672,73401],{"class":364},[137,83674,4168],{"class":157},[137,83676,83677],{"class":139,"line":291},[137,83678,516],{"emptyLinePlaceholder":515},[137,83680,83681],{"class":139,"line":297},[137,83682,83683],{"class":308},"\u002F\u002F Create a new post\n",[137,83685,83686,83688,83690,83692,83695,83698],{"class":139,"line":302},[137,83687,54992],{"class":143},[137,83689,83628],{"class":157},[137,83691,12],{"class":147},[137,83693,83694],{"class":157},"({ text: ",[137,83696,83697],{"class":284},"\"Hello from my AT Protocol app!\"",[137,83699,4168],{"class":157},[27,83701,83702,83705,83706,83709,83710,83713],{},[22,83703,83704],{},"agent.getProfile()"," fetches profile data from the network. ",[22,83707,83708],{},"agent.getAuthorFeed()"," retrieves recent posts. ",[22,83711,83712],{},"agent.post()"," writes a new post to the user's data repository. The agent knows which PDS to talk to because of the OAuth session - you don't have to think about the underlying routing.",[123,83715,83717],{"id":83716},"running-it","Running It",[128,83719,83720],{"className":8665,"code":21926,"language":8667,"meta":133,"style":133},[22,83721,83722],{"__ignoreMap":133},[137,83723,83724,83726,83728],{"class":139,"line":140},[137,83725,9536],{"class":147},[137,83727,9578],{"class":284},[137,83729,9581],{"class":284},[27,83731,61028,83732,83734,83735,83738],{},[22,83733,13068],{},", click ",[42,83736,83737],{},"Login with Bluesky",", enter your handle, approve access on Bluesky's side, and you'll land back on the app - logged in, with your profile and a form to post.",[104,83740,83742],{"id":83741},"from-bluesky-client-to-your-own-social-media","From Bluesky Client to Your Own Social Media",[27,83744,83745,83746,83749],{},"So far, we've built a client that talks to Bluesky. But here's where it gets interesting: you're not limited to being a Bluesky client. The AT Protocol is designed so that ",[42,83747,83748],{},"you can build your own social media experience"," on top of the same network. Let me explain what that means in simple terms.",[123,83751,83753],{"id":83752},"the-app-we-built-is-already-more-than-a-client","The App We Built Is Already More Than a Client",[27,83755,83756,83757,83760],{},"Look at what our little demo app does. It authenticates a user, reads their data, and writes posts to their personal repository. At no point did we talk to \"Bluesky the company.\" We talked to the ",[42,83758,83759],{},"protocol",". The user's data lives on their PDS, and we accessed it through standard AT Protocol APIs.",[27,83762,83763],{},"This means our app is already a separate, independent way to interact with the network. It's like building a different email client - same emails, same protocol, different experience.",[123,83765,83767],{"id":83766},"building-your-own-experience","Building Your Own Experience",[27,83769,83770],{},"But what if you don't want a Twitter-like feed at all? What if you want to build something completely different - a recipe-sharing platform, a book review site, a collaborative journal?",[27,83772,83773,83774,83777,83778,83781,83782,83785],{},"The AT Protocol supports this through ",[42,83775,83776],{},"Lexicons"," - a schema system that lets you define your own data types. Bluesky uses Lexicons like ",[22,83779,83780],{},"app.bsky.feed.post"," for posts and ",[22,83783,83784],{},"app.bsky.actor.profile"," for profiles. But you can create your own.",[27,83787,83788],{},"Say you want to build a recipe-sharing app. You'd define a schema like this:",[128,83790,83792],{"className":5155,"code":83791,"language":5157,"meta":133,"style":133},"{\n    \"lexicon\": 1,\n    \"id\": \"com.myrecipes.recipe\",\n    \"defs\": {\n        \"main\": {\n            \"type\": \"record\",\n            \"key\": \"tid\",\n            \"record\": {\n                \"type\": \"object\",\n                \"required\": [\"title\", \"ingredients\", \"steps\", \"createdAt\"],\n                \"properties\": {\n                    \"title\": { \"type\": \"string\", \"maxLength\": 200 },\n                    \"ingredients\": {\n                        \"type\": \"array\",\n                        \"items\": { \"type\": \"string\" }\n                    },\n                    \"steps\": {\n                        \"type\": \"array\",\n                        \"items\": { \"type\": \"string\" }\n                    },\n                    \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n                }\n            }\n        }\n    }\n}\n",[22,83793,83794,83798,83809,83820,83827,83834,83846,83858,83865,83876,83902,83909,83933,83940,83951,83966,83970,83977,83987,84001,84005,84030,84034,84038,84042,84046],{"__ignoreMap":133},[137,83795,83796],{"class":139,"line":140},[137,83797,15971],{"class":157},[137,83799,83800,83803,83805,83807],{"class":139,"line":173},[137,83801,83802],{"class":364},"    \"lexicon\"",[137,83804,726],{"class":157},[137,83806,6065],{"class":364},[137,83808,1961],{"class":157},[137,83810,83811,83813,83815,83818],{"class":139,"line":188},[137,83812,73353],{"class":364},[137,83814,726],{"class":157},[137,83816,83817],{"class":284},"\"com.myrecipes.recipe\"",[137,83819,1961],{"class":157},[137,83821,83822,83825],{"class":139,"line":269},[137,83823,83824],{"class":364},"    \"defs\"",[137,83826,1819],{"class":157},[137,83828,83829,83832],{"class":139,"line":278},[137,83830,83831],{"class":364},"        \"main\"",[137,83833,1819],{"class":157},[137,83835,83836,83839,83841,83844],{"class":139,"line":291},[137,83837,83838],{"class":364},"            \"type\"",[137,83840,726],{"class":157},[137,83842,83843],{"class":284},"\"record\"",[137,83845,1961],{"class":157},[137,83847,83848,83851,83853,83856],{"class":139,"line":297},[137,83849,83850],{"class":364},"            \"key\"",[137,83852,726],{"class":157},[137,83854,83855],{"class":284},"\"tid\"",[137,83857,1961],{"class":157},[137,83859,83860,83863],{"class":139,"line":302},[137,83861,83862],{"class":364},"            \"record\"",[137,83864,1819],{"class":157},[137,83866,83867,83869,83871,83874],{"class":139,"line":662},[137,83868,67636],{"class":364},[137,83870,726],{"class":157},[137,83872,83873],{"class":284},"\"object\"",[137,83875,1961],{"class":157},[137,83877,83878,83881,83883,83885,83887,83890,83892,83895,83897,83900],{"class":139,"line":667},[137,83879,83880],{"class":364},"                \"required\"",[137,83882,29669],{"class":157},[137,83884,67351],{"class":284},[137,83886,164],{"class":157},[137,83888,83889],{"class":284},"\"ingredients\"",[137,83891,164],{"class":157},[137,83893,83894],{"class":284},"\"steps\"",[137,83896,164],{"class":157},[137,83898,83899],{"class":284},"\"createdAt\"",[137,83901,21916],{"class":157},[137,83903,83904,83907],{"class":139,"line":786},[137,83905,83906],{"class":364},"                \"properties\"",[137,83908,1819],{"class":157},[137,83910,83911,83914,83916,83918,83920,83922,83924,83927,83929,83931],{"class":139,"line":798},[137,83912,83913],{"class":364},"                    \"title\"",[137,83915,54941],{"class":157},[137,83917,67370],{"class":364},[137,83919,726],{"class":157},[137,83921,67375],{"class":284},[137,83923,164],{"class":157},[137,83925,83926],{"class":364},"\"maxLength\"",[137,83928,726],{"class":157},[137,83930,9171],{"class":364},[137,83932,31293],{"class":157},[137,83934,83935,83938],{"class":139,"line":803},[137,83936,83937],{"class":364},"                    \"ingredients\"",[137,83939,1819],{"class":157},[137,83941,83942,83945,83947,83949],{"class":139,"line":931},[137,83943,83944],{"class":364},"                        \"type\"",[137,83946,726],{"class":157},[137,83948,67489],{"class":284},[137,83950,1961],{"class":157},[137,83952,83953,83956,83958,83960,83962,83964],{"class":139,"line":1568},[137,83954,83955],{"class":364},"                        \"items\"",[137,83957,54941],{"class":157},[137,83959,67370],{"class":364},[137,83961,726],{"class":157},[137,83963,67375],{"class":284},[137,83965,1115],{"class":157},[137,83967,83968],{"class":139,"line":1573},[137,83969,36372],{"class":157},[137,83971,83972,83975],{"class":139,"line":1578},[137,83973,83974],{"class":364},"                    \"steps\"",[137,83976,1819],{"class":157},[137,83978,83979,83981,83983,83985],{"class":139,"line":1588},[137,83980,83944],{"class":364},[137,83982,726],{"class":157},[137,83984,67489],{"class":284},[137,83986,1961],{"class":157},[137,83988,83989,83991,83993,83995,83997,83999],{"class":139,"line":1601},[137,83990,83955],{"class":364},[137,83992,54941],{"class":157},[137,83994,67370],{"class":364},[137,83996,726],{"class":157},[137,83998,67375],{"class":284},[137,84000,1115],{"class":157},[137,84002,84003],{"class":139,"line":3802},[137,84004,36372],{"class":157},[137,84006,84007,84010,84012,84014,84016,84018,84020,84023,84025,84028],{"class":139,"line":3808},[137,84008,84009],{"class":364},"                    \"createdAt\"",[137,84011,54941],{"class":157},[137,84013,67370],{"class":364},[137,84015,726],{"class":157},[137,84017,67375],{"class":284},[137,84019,164],{"class":157},[137,84021,84022],{"class":364},"\"format\"",[137,84024,726],{"class":157},[137,84026,84027],{"class":284},"\"datetime\"",[137,84029,1115],{"class":157},[137,84031,84032],{"class":139,"line":3822},[137,84033,34372],{"class":157},[137,84035,84036],{"class":139,"line":3827},[137,84037,760],{"class":157},[137,84039,84040],{"class":139,"line":3832},[137,84041,1966],{"class":157},[137,84043,84044],{"class":139,"line":3840},[137,84045,294],{"class":157},[137,84047,84048],{"class":139,"line":3846},[137,84049,510],{"class":157},[27,84051,84052,84053,84055,84056,84059],{},"That schema says: \"A recipe has a title, a list of ingredients, a list of steps, and a timestamp.\" The ",[22,84054,31478],{}," uses reverse-DNS naming (",[22,84057,84058],{},"com.myrecipes.recipe",") so it doesn't collide with anyone else's schemas.",[27,84061,84062,84063,84066],{},"Now here's the key part - when a user saves a recipe through your app, it gets written to ",[42,84064,84065],{},"their PDS",", just like a Bluesky post does. The recipe lives in the user's personal data repository, not on your server. The user owns it.",[123,84068,84070],{"id":84069},"how-would-you-actually-build-this","How Would You Actually Build This?",[27,84072,84073],{},"Let's walk through the pieces. You already know most of them from the demo app we built.",[27,84075,84076,84079,84080,1017],{},[42,84077,84078],{},"Step 1: Users log in with OAuth"," - exactly the same as what we already built. The user authenticates with their PDS. You get an ",[22,84081,83134],{},[27,84083,84084,84087,84088,84090],{},[42,84085,84086],{},"Step 2: Write custom records to the user's repository."," Instead of ",[22,84089,83712],{}," (which is a Bluesky shortcut), you use the lower-level API:",[128,84092,84094],{"className":13299,"code":84093,"language":13301,"meta":133,"style":133},"await agent.com.atproto.repo.putRecord({\n    repo: agent.assertDid,\n    collection: \"com.myrecipes.recipe\",\n    rkey: TID.nextStr(),\n    record: {\n        $type: \"com.myrecipes.recipe\",\n        title: \"Banana Bread\",\n        ingredients: [\"3 bananas\", \"1 cup sugar\", \"2 cups flour\", \"1 egg\"],\n        steps: [\"Mash bananas\", \"Mix ingredients\", \"Bake at 180°C for 50 min\"],\n        createdAt: new Date().toISOString(),\n    },\n});\n",[22,84095,84096,84108,84113,84122,84138,84143,84152,84162,84187,84207,84222,84226],{"__ignoreMap":133},[137,84097,84098,84100,84103,84106],{"class":139,"line":140},[137,84099,54992],{"class":143},[137,84101,84102],{"class":157}," agent.com.atproto.repo.",[137,84104,84105],{"class":147},"putRecord",[137,84107,3175],{"class":157},[137,84109,84110],{"class":139,"line":173},[137,84111,84112],{"class":157},"    repo: agent.assertDid,\n",[137,84114,84115,84118,84120],{"class":139,"line":188},[137,84116,84117],{"class":157},"    collection: ",[137,84119,83817],{"class":284},[137,84121,1961],{"class":157},[137,84123,84124,84127,84130,84132,84135],{"class":139,"line":269},[137,84125,84126],{"class":157},"    rkey: ",[137,84128,84129],{"class":364},"TID",[137,84131,1017],{"class":157},[137,84133,84134],{"class":147},"nextStr",[137,84136,84137],{"class":157},"(),\n",[137,84139,84140],{"class":139,"line":278},[137,84141,84142],{"class":157},"    record: {\n",[137,84144,84145,84148,84150],{"class":139,"line":291},[137,84146,84147],{"class":157},"        $type: ",[137,84149,83817],{"class":284},[137,84151,1961],{"class":157},[137,84153,84154,84157,84160],{"class":139,"line":297},[137,84155,84156],{"class":157},"        title: ",[137,84158,84159],{"class":284},"\"Banana Bread\"",[137,84161,1961],{"class":157},[137,84163,84164,84167,84170,84172,84175,84177,84180,84182,84185],{"class":139,"line":302},[137,84165,84166],{"class":157},"        ingredients: [",[137,84168,84169],{"class":284},"\"3 bananas\"",[137,84171,164],{"class":157},[137,84173,84174],{"class":284},"\"1 cup sugar\"",[137,84176,164],{"class":157},[137,84178,84179],{"class":284},"\"2 cups flour\"",[137,84181,164],{"class":157},[137,84183,84184],{"class":284},"\"1 egg\"",[137,84186,21916],{"class":157},[137,84188,84189,84192,84195,84197,84200,84202,84205],{"class":139,"line":662},[137,84190,84191],{"class":157},"        steps: [",[137,84193,84194],{"class":284},"\"Mash bananas\"",[137,84196,164],{"class":157},[137,84198,84199],{"class":284},"\"Mix ingredients\"",[137,84201,164],{"class":157},[137,84203,84204],{"class":284},"\"Bake at 180°C for 50 min\"",[137,84206,21916],{"class":157},[137,84208,84209,84212,84214,84216,84218,84220],{"class":139,"line":667},[137,84210,84211],{"class":157},"        createdAt: ",[137,84213,1361],{"class":143},[137,84215,77686],{"class":147},[137,84217,17766],{"class":157},[137,84219,77691],{"class":147},[137,84221,84137],{"class":157},[137,84223,84224],{"class":139,"line":786},[137,84225,775],{"class":157},[137,84227,84228],{"class":139,"line":798},[137,84229,5422],{"class":157},[27,84231,84232,84233,84235],{},"That recipe is now stored in the user's PDS under the ",[22,84234,84058],{}," collection. It sits right alongside their Bluesky posts, their likes, their follows - all in the same personal repository but in a different collection.",[27,84237,84238,84241,84242,84245],{},[42,84239,84240],{},"Step 3: Listen to the network."," This is where your app becomes a real platform. The AT Protocol provides a ",[42,84243,84244],{},"firehose"," - a real-time stream of every record being created across the entire network. You subscribe to it and filter for your record type:",[128,84247,84249],{"className":13299,"code":84248,"language":13301,"meta":133,"style":133},"new Firehose({\n    filterCollections: [\"com.myrecipes.recipe\"],\n    handleEvent: async (evt) => {\n        \u002F\u002F A user somewhere on the network just saved a recipe\n        \u002F\u002F Store it in your database for search, discovery, recommendations\n    },\n});\n",[22,84250,84251,84260,84269,84289,84294,84299,84303],{"__ignoreMap":133},[137,84252,84253,84255,84258],{"class":139,"line":140},[137,84254,1361],{"class":143},[137,84256,84257],{"class":147}," Firehose",[137,84259,3175],{"class":157},[137,84261,84262,84265,84267],{"class":139,"line":173},[137,84263,84264],{"class":157},"    filterCollections: [",[137,84266,83817],{"class":284},[137,84268,21916],{"class":157},[137,84270,84271,84274,84276,84278,84280,84283,84285,84287],{"class":139,"line":188},[137,84272,84273],{"class":147},"    handleEvent",[137,84275,726],{"class":157},[137,84277,15050],{"class":143},[137,84279,158],{"class":157},[137,84281,84282],{"class":161},"evt",[137,84284,219],{"class":157},[137,84286,222],{"class":143},[137,84288,256],{"class":157},[137,84290,84291],{"class":139,"line":269},[137,84292,84293],{"class":308},"        \u002F\u002F A user somewhere on the network just saved a recipe\n",[137,84295,84296],{"class":139,"line":278},[137,84297,84298],{"class":308},"        \u002F\u002F Store it in your database for search, discovery, recommendations\n",[137,84300,84301],{"class":139,"line":291},[137,84302,775],{"class":157},[137,84304,84305],{"class":139,"line":297},[137,84306,5422],{"class":157},[27,84308,84309,84310,84312,84313,84316],{},"Your app collects recipes from everyone on the network who uses the ",[22,84311,84058],{}," schema. You store them in your own database, index them, and build features on top: search, trending recipes, personalised recommendations. This is the ",[42,84314,84315],{},"AppView"," pattern - your app becomes a specialised view of the network's data.",[27,84318,84319,84322],{},[42,84320,84321],{},"Step 4: Other apps can read the same data."," Because the recipes live in the user's PDS using a published schema, anyone else can build a different recipe app that reads the exact same data. Maybe your app focuses on quick weeknight dinners, and someone else builds one focused on baking. Same recipes, different experiences.",[27,84324,84325],{},[63,84326],{"alt":84327,"src":84328},"How Would You Actually Build This","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1770893348\u002Fblog\u002Fthe-protocol-behind-bluesky-rethinking-social-media-architecture\u002Fhow-would-you-actually-build-this_zpwabj",[123,84330,84332],{"id":84331},"the-big-picture","The Big Picture",[27,84334,84335],{},"Here's a simple way to think about the whole thing:",[45740,84337,84338,84352],{},[45743,84339,84340],{},[45746,84341,84342,84347],{},[45749,84343,84344],{},[42,84345,84346],{},"Traditional Platform",[45749,84348,84349],{},[42,84350,84351],{},"AT Protocol",[45762,84353,84354,84362,84370,84378,84386],{},[45746,84355,84356,84359],{},[45767,84357,84358],{},"Your data lives on the platform's servers",[45767,84360,84361],{},"Your data lives on your PDS",[45746,84363,84364,84367],{},[45767,84365,84366],{},"One app, one experience",[45767,84368,84369],{},"Many apps, same data",[45746,84371,84372,84375],{},[45767,84373,84374],{},"Platform decides the algorithm",[45767,84376,84377],{},"Each app chooses its own",[45746,84379,84380,84383],{},[45767,84381,84382],{},"You leave = you lose everything",[45767,84384,84385],{},"You leave = you take everything",[45746,84387,84388,84391],{},[45767,84389,84390],{},"Only the platform can build features",[45767,84392,84393],{},"Anyone can build an AppView",[27,84395,84396],{},"The app we built earlier is the simplest version of this: one app talking to users' repositories. But the architecture scales all the way up to a full social media platform with its own data types, its own feed algorithms, and its own community - all while users keep ownership of their data and the freedom to leave.",[27,84398,84399],{},"That's the real promise of the AT Protocol. Not just \"another Bluesky client,\" but a foundation for an entirely different kind of social internet.",[104,84401,82351],{"id":82350},[27,84403,84404],{},"The AT Protocol is still evolving. The core specs are being standardised through the IETF, which signals this is meant to become an open standard - not just a Bluesky implementation detail.",[27,84406,84407],{},"What I find most interesting is the architectural bet: separate the data layer from the application layer, give users real portability, and let apps compete on experience rather than on locking in users. Whether it succeeds at scale is an open question. Federation and decentralisation always bring trade-offs - complexity, consistency challenges, moderation at scale.",[27,84409,84410],{},"But the fact that you can already build working apps against this protocol, with proper OAuth and genuine data portability, puts it ahead of most \"decentralisation\" projects. The ambition is real, and the code is running.",[27,84412,84413,84414,1017],{},"The complete source code is available in the ",[45,84415,71386],{"href":82715,"target":2716,"rel":84416},[2718,2719],[2617,84418,84419],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":133,"searchDepth":173,"depth":173,"links":84421},[84422,84423,84424,84425,84426,84427,84428,84436,84442],{"id":82476,"depth":173,"text":82477},{"id":82521,"depth":173,"text":82522},{"id":82542,"depth":173,"text":82543},{"id":82574,"depth":173,"text":82575},{"id":82612,"depth":173,"text":82613},{"id":82658,"depth":173,"text":82659},{"id":82708,"depth":173,"text":82709,"children":84429},[84430,84431,84432,84433,84434,84435],{"id":47891,"depth":188,"text":47892},{"id":82855,"depth":188,"text":82856},{"id":82893,"depth":188,"text":82894},{"id":83297,"depth":188,"text":83298},{"id":83589,"depth":188,"text":83590},{"id":83716,"depth":188,"text":83717},{"id":83741,"depth":173,"text":83742,"children":84437},[84438,84439,84440,84441],{"id":83752,"depth":188,"text":83753},{"id":83766,"depth":188,"text":83767},{"id":84069,"depth":188,"text":84070},{"id":84331,"depth":188,"text":84332},{"id":82350,"depth":173,"text":82351},"Explore the AT Protocol powering Bluesky - a decentralised social networking protocol that gives users real ownership over their data and identity. Learn how it works, how it differs from blockchain and Mastodon, and build a simple OAuth app against it.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1770893361\u002Fblog\u002Fthe-protocol-behind-bluesky-rethinking-social-media-architecture\u002Fthe-protocol-behind-bluesky-rethinking-social-media-architecture-2_bqif19",[84351,82469,84446,84447,84448,84449,82683,83130,84450,84451,84452,82624,84453,84454,83776,84315,84244,84455,2669,12817,84456,84457,84458,84459],"decentralised social media","federated protocol","data portability","personal data server","decentralised identifier","OAuth authentication","social networking","Mastodon comparison","blockchain alternative","relay","social media architecture","user data ownership","open protocol","IETF standardisation",{},"\u002F2026\u002F02\u002F15\u002Fthe-protocol-behind-bluesky-rethinking-social-media-architecture","15th February 2026",{"title":82427,"description":84443},"2026\u002F02\u002F15\u002Fthe-protocol-behind-bluesky-rethinking-social-media-architecture","RFgV2cspCpN8ANFqXxqp2U19fOd7b1GkiOlM_UFPcfw",{"id":84467,"title":84468,"articleTags":84469,"author":11,"blog":12,"body":84470,"description":88059,"extension":2649,"image":88060,"keywords":88061,"meta":88073,"navigation":515,"path":88074,"published":88075,"readTime":302,"seo":88076,"stem":88077,"type":2662,"__hash__":88078},"content\u002F2026\u002F02\u002F22\u002Fagentic-workflows-write-github-actions-in-markdown.md","Agentic Workflows: Write GitHub Actions in Markdown",[75905,27886,71449],{"type":14,"value":84471,"toc":88009},[84472,84475,84489,84491,84495,84500,84503,84518,84521,84525,84549,84558,84572,84585,84593,84597,84600,84654,84661,84663,84666,84670,84676,84689,84692,84705,84708,84712,84718,84735,84756,84760,84774,84778,84781,84830,84844,84855,84872,84876,84879,84884,84911,84916,84946,84950,84953,85044,85047,85093,85097,85100,85241,85244,85251,85260,85487,85490,85516,85523,85532,85580,85614,85641,85646,85653,85742,85749,85755,85764,85782,85797,85829,85832,85837,85880,85885,85927,85932,85974,85981,86061,86067,86074,86201,86204,86304,86314,86320,86325,86340,86347,86351,86354,86417,86420,86424,86431,86440,86454,86463,86466,86486,86495,86501,86507,86517,86524,86534,86603,86621,86624,86630,86647,86653,86659,86665,86671,86677,86681,86688,86704,86707,86714,86722,86726,86729,86733,86739,86749,87002,87006,87011,87016,87206,87210,87215,87220,87390,87394,87399,87582,87586,87591,87600,87771,87775,87780,87785,87971,87973,87976,87979,87982,88003,88006],[17,84473,84468],{"id":84474},"agentic-workflows-write-github-actions-in-markdown",[27,84476,84477],{},[30,84478,84479,36,84481,40,84483],{},[33,84480],{"value":35},[33,84482],{"value":39},[42,84484,84485],{},[45,84486,84487],{"href":47},[33,84488],{"value":50},[52,84490],{":tags":54},[56,84492],{":audio-src":84493,":transcript-src":84494},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F02\u002F22\u002Fagentic-workflows-write-github-actions-in-markdown\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F02\u002F22\u002Fagentic-workflows-write-github-actions-in-markdown\u002Fsummary.json",[27,84496,84497],{},[63,84498],{"alt":12847,"src":84499},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1771503278\u002Fblog\u002Fgithub-agentic-workflows-build-github-actions-in-markdown-with-ai-agents\u002Fgithub-agentic-workflows-build-github-actions-in-markdown-with-ai-agents_r153nu",[27,84501,84502],{},"What if your GitHub repository could triage its own issues, review pull requests for quality, generate weekly health reports, and investigate CI failures - all without you writing a single line of YAML?",[27,84504,84505,84506,84513,84514,84517],{},"Last week, GitHub launched the technical preview of ",[45,84507,84510],{"href":84508,"target":2716,"rel":84509},"https:\u002F\u002Fgithub.blog\u002Fai-and-ml\u002Fautomate-repository-tasks-with-github-agentic-workflows\u002F",[2718,2719],[42,84511,84512],{},"GitHub Agentic Workflows",". It's a new way to automate repository tasks using AI agents that run directly inside GitHub Actions. Instead of writing complex YAML configuration files, you describe what you want to happen in ",[42,84515,84516],{},"plain Markdown",", and an AI agent figures out how to do it.",[27,84519,84520],{},"I've been experimenting with it on this blog's repository, building workflows that review my articles, triage issues, and generate content reports. In this article, I'll walk you through everything: what agentic workflows are, how they work, and how to build your own step by step.",[104,84522,84524],{"id":84523},"what-are-github-agentic-workflows","What Are GitHub Agentic Workflows?",[27,84526,84527,84528,164,84535,14528,84538,84541,84542,114,84545,84548],{},"GitHub Agentic Workflows is a collaboration between ",[45,84529,84532],{"href":84530,"target":2716,"rel":84531},"https:\u002F\u002Fgithubnext.com\u002Fprojects\u002Fagentic-workflows\u002F",[2718,2719],[42,84533,84534],{},"GitHub Next",[42,84536,84537],{},"Microsoft Research",[42,84539,84540],{},"Azure Core Upstream",". The idea is simple: instead of scripting every step of an automation in YAML, you define the ",[42,84543,84544],{},"outcome",[42,84546,84547],{},"guardrails",", and an AI agent executes the work.",[27,84550,84551,84552,84554,84555,84557],{},"Each workflow is a Markdown file (",[22,84553,80806],{},") placed in ",[22,84556,76051],{},". It has two parts:",[2569,84559,84560,84566],{},[1006,84561,84562,84565],{},[42,84563,84564],{},"YAML frontmatter"," - Defines triggers, permissions, tools, and constraints",[1006,84567,84568,84571],{},[42,84569,84570],{},"Markdown body"," - Natural language instructions telling the AI what to do",[27,84573,4737,84574,84577,84578,84580,84581,84584],{},[22,84575,84576],{},"gh aw"," CLI compiles these ",[22,84579,80806],{}," files into standard GitHub Actions ",[22,84582,84583],{},".lock.yml"," workflows. When triggered, an AI coding agent (GitHub Copilot, Claude, or Codex) reads your Markdown instructions, uses declared tools to interact with your repository, and produces results through controlled output channels.",[27,84586,84587,84588,1017],{},"The project is open source at ",[45,84589,84592],{"href":84590,"target":2716,"rel":84591},"https:\u002F\u002Fgithub.com\u002Fgithub\u002Fgh-aw",[2718,2719],"github.com\u002Fgithub\u002Fgh-aw",[104,84594,84596],{"id":84595},"how-they-work-behind-the-scenes","How They Work Behind the Scenes",[27,84598,84599],{},"Here's what happens when an agentic workflow runs:",[2569,84601,84602,84609,84618,84625,84632,84644,84651],{},[1006,84603,84604,84605,84608],{},"A ",[42,84606,84607],{},"trigger fires"," - a push event, an issue being opened, a schedule, or a manual dispatch",[1006,84610,84611,84612,84614,84615,84617],{},"The compiled ",[22,84613,84583],{}," workflow starts on ",[42,84616,80385],{}," infrastructure",[1006,84619,84620,84621,84624],{},"A containerised ",[42,84622,84623],{},"sandbox"," is created with strict security boundaries",[1006,84626,84627,84628,84631],{},"The AI ",[42,84629,84630],{},"engine"," reads your Markdown instructions",[1006,84633,84634,84635,84638,84639],{},"The agent interacts with the repository through ",[42,84636,84637],{},"tools"," exposed via the ",[45,84640,84643],{"href":84641,"target":2716,"rel":84642},"https:\u002F\u002Fmodelcontextprotocol.io\u002F",[2718,2719],"Model Context Protocol (MCP)",[1006,84645,84646,84647,84650],{},"Any write operations go through ",[42,84648,84649],{},"safe outputs"," - pre-approved, reviewable GitHub operations",[1006,84652,84653],{},"A separate, permission-scoped job executes the actual writes (creating issues, PRs, comments, labels)",[27,84655,84656,84657,84660],{},"The key principle is ",[42,84658,84659],{},"read-only by default",". The AI agent itself never gets direct write access to your repository. Every write operation must pass through the safe-outputs pipeline. Even if the agent were fully compromised, it couldn't directly modify your repository.",[104,84662,66831],{"id":66828},[27,84664,84665],{},"Before we start, you'll need a few things installed.",[123,84667,84669],{"id":84668},"installing-the-github-cli","Installing the GitHub CLI",[27,84671,84672,84673,84675],{},"The GitHub CLI (",[22,84674,79757],{},") is required. If you don't have it, install it with Homebrew:",[128,84677,84679],{"className":8665,"code":84678,"language":8667,"meta":133,"style":133},"brew install gh\n",[22,84680,84681],{"__ignoreMap":133},[137,84682,84683,84685,84687],{"class":139,"line":140},[137,84684,56827],{"class":147},[137,84686,10268],{"class":284},[137,84688,80638],{"class":284},[27,84690,84691],{},"Then authenticate:",[128,84693,84695],{"className":8665,"code":84694,"language":8667,"meta":133,"style":133},"gh auth login\n",[22,84696,84697],{"__ignoreMap":133},[137,84698,84699,84701,84703],{"class":139,"line":140},[137,84700,79757],{"class":147},[137,84702,23716],{"class":284},[137,84704,80647],{"class":284},[27,84706,84707],{},"This opens your browser and prompts you to log in with GitHub.",[123,84709,84711],{"id":84710},"installing-the-gh-aw-extension","Installing the gh-aw Extension",[27,84713,4737,84714,84717],{},[22,84715,84716],{},"gh-aw"," extension adds the agentic workflow commands to the CLI. Install it with:",[128,84719,84721],{"className":8665,"code":84720,"language":8667,"meta":133,"style":133},"gh extension install github\u002Fgh-aw\n",[22,84722,84723],{"__ignoreMap":133},[137,84724,84725,84727,84730,84732],{"class":139,"line":140},[137,84726,79757],{"class":147},[137,84728,84729],{"class":284}," extension",[137,84731,10268],{"class":284},[137,84733,84734],{"class":284}," github\u002Fgh-aw\n",[27,84736,84737,84738,84741,84742,84744,84745,84747,84748,164,84751,14528,84753,1017],{},"This installs the extension ",[42,84739,84740],{},"locally on your machine"," as a plugin for ",[22,84743,79757],{},". It adds ",[22,84746,84576],{}," subcommands like ",[22,84749,84750],{},"compile",[22,84752,62084],{},[22,84754,84755],{},"secrets set",[104,84757,84759],{"id":84758},"setting-up-authentication","Setting Up Authentication",[27,84761,84762,84763,84766,84767,3955,84770,84773],{},"Agentic workflows need an AI engine to run. The default is ",[42,84764,84765],{},"GitHub Copilot",", but you can also use ",[42,84768,84769],{},"Claude",[42,84771,84772],{},"Codex",". Each engine requires its own secret stored in your repository.",[123,84775,84777],{"id":84776},"creating-a-fine-grained-pat-for-copilot","Creating a Fine-Grained PAT (for Copilot)",[27,84779,84780],{},"If you're using the default Copilot engine, you need a Personal Access Token:",[2569,84782,84783,84796,84803,84812,84817,84820,84823],{},[1006,84784,84785,84786,84789,84790,84793,84794],{},"Go to ",[42,84787,84788],{},"GitHub.com"," > click your ",[42,84791,84792],{},"profile picture"," > ",[42,84795,80319],{},[1006,84797,84798,84799,84802],{},"Scroll to ",[42,84800,84801],{},"Developer settings"," (at the very bottom of the left sidebar)",[1006,84804,84805,84806,84793,84809],{},"Click ",[42,84807,84808],{},"Personal access tokens",[42,84810,84811],{},"Fine-grained tokens",[1006,84813,84805,84814],{},[42,84815,84816],{},"Generate new token",[1006,84818,84819],{},"Give it a name (e.g., \"Agentic Workflows\")",[1006,84821,84822],{},"Set an expiration date",[1006,84824,84825,84826,84829],{},"Under ",[42,84827,84828],{},"Repository access",", select the repos you want it to work with",[27,84831,84832,84833,84836,84837,84840,84841,84843],{},"Now here's something important that's easy to miss: the permission you need - ",[42,84834,84835],{},"Copilot Requests"," - is under ",[42,84838,84839],{},"Account permissions",", not Repository permissions. Most people look only at the Repository permissions section and can't find it. Scroll down past Repository permissions to the ",[42,84842,84839],{}," section, and you'll see \"Copilot Requests\" there.",[3244,84845,84846],{},[27,84847,84848,84850,84851,84854],{},[42,84849,3250],{}," This permission only appears if you have an ",[42,84852,84853],{},"active GitHub Copilot subscription",". If you don't see it, you'll need to use a different engine.",[2569,84856,84857,84864,84869],{"start":302},[1006,84858,84825,84859,84861,84862],{},[42,84860,84839],{},", enable ",[42,84863,84835],{},[1006,84865,84805,84866],{},[42,84867,84868],{},"Generate token",[1006,84870,84871],{},"Copy the token immediately - you won't see it again",[123,84873,84875],{"id":84874},"storing-the-secret","Storing the Secret",[27,84877,84878],{},"You can store the token either through the CLI or the GitHub dashboard.",[27,84880,84881],{},[42,84882,84883],{},"Option A - CLI:",[128,84885,84887],{"className":8665,"code":84886,"language":8667,"meta":133,"style":133},"gh aw secrets set COPILOT_GITHUB_TOKEN --value \"github_pat_xxxxxxxxxxxx...\"\n",[22,84888,84889],{"__ignoreMap":133},[137,84890,84891,84893,84896,84899,84902,84905,84908],{"class":139,"line":140},[137,84892,79757],{"class":147},[137,84894,84895],{"class":284}," aw",[137,84897,84898],{"class":284}," secrets",[137,84900,84901],{"class":284}," set",[137,84903,84904],{"class":284}," COPILOT_GITHUB_TOKEN",[137,84906,84907],{"class":364}," --value",[137,84909,84910],{"class":284}," \"github_pat_xxxxxxxxxxxx...\"\n",[27,84912,84913],{},[42,84914,84915],{},"Option B - GitHub Dashboard:",[2569,84917,84918,84929,84934,84941],{},[1006,84919,84920,84921,84793,84923,84793,84926],{},"Go to your repo > ",[42,84922,80319],{},[42,84924,84925],{},"Secrets and variables",[42,84927,84928],{},"Actions",[1006,84930,84805,84931],{},[42,84932,84933],{},"New repository secret",[1006,84935,84936,84937,84940],{},"Name: ",[22,84938,84939],{},"COPILOT_GITHUB_TOKEN",", Value: paste your token",[1006,84942,84805,84943],{},[42,84944,84945],{},"Add secret",[123,84947,84949],{"id":84948},"alternative-engines","Alternative Engines",[27,84951,84952],{},"If you don't have a Copilot subscription or prefer a different engine, you have two other options:",[45740,84954,84955,84971],{},[45743,84956,84957],{},[45746,84958,84959,84962,84965,84968],{},[45749,84960,84961],{},"Engine",[45749,84963,84964],{},"Frontmatter",[45749,84966,84967],{},"Secret Name",[45749,84969,84970],{},"How to Get It",[45762,84972,84973,84998,85021],{},[45746,84974,84975,84981,84991,84995],{},[45767,84976,84977,84980],{},[42,84978,84979],{},"Copilot"," (default)",[45767,84982,84983,84984,84987,84988],{},"Omit ",[22,84985,84986],{},"engine:"," or use ",[22,84989,84990],{},"engine: copilot",[45767,84992,84993],{},[22,84994,84939],{},[45767,84996,84997],{},"Fine-grained PAT with Copilot Requests permission",[45746,84999,85000,85004,85009,85014],{},[45767,85001,85002],{},[42,85003,84769],{},[45767,85005,85006],{},[22,85007,85008],{},"engine: claude",[45767,85010,85011],{},[22,85012,85013],{},"ANTHROPIC_API_KEY",[45767,85015,85016],{},[45,85017,85020],{"href":85018,"target":2716,"rel":85019},"https:\u002F\u002Fplatform.claude.com\u002Fsettings\u002Fkeys",[2718,2719],"platform.claude.com\u002Fsettings\u002Fkeys",[45746,85022,85023,85027,85032,85037],{},[45767,85024,85025],{},[42,85026,84772],{},[45767,85028,85029],{},[22,85030,85031],{},"engine: codex",[45767,85033,85034],{},[22,85035,85036],{},"OPENAI_API_KEY",[45767,85038,85039],{},[45,85040,85043],{"href":85041,"target":2716,"rel":85042},"https:\u002F\u002Fplatform.openai.com\u002Fapi-keys",[2718,2719],"platform.openai.com\u002Fapi-keys",[27,85045,85046],{},"Store the secret the same way:",[128,85048,85050],{"className":8665,"code":85049,"language":8667,"meta":133,"style":133},"gh aw secrets set ANTHROPIC_API_KEY --value \"sk-ant-xxxxxxxxxxxx...\"\n# or\ngh aw secrets set OPENAI_API_KEY --value \"sk-xxxxxxxxxxxx...\"\n",[22,85051,85052,85070,85075],{"__ignoreMap":133},[137,85053,85054,85056,85058,85060,85062,85065,85067],{"class":139,"line":140},[137,85055,79757],{"class":147},[137,85057,84895],{"class":284},[137,85059,84898],{"class":284},[137,85061,84901],{"class":284},[137,85063,85064],{"class":284}," ANTHROPIC_API_KEY",[137,85066,84907],{"class":364},[137,85068,85069],{"class":284}," \"sk-ant-xxxxxxxxxxxx...\"\n",[137,85071,85072],{"class":139,"line":173},[137,85073,85074],{"class":308},"# or\n",[137,85076,85077,85079,85081,85083,85085,85088,85090],{"class":139,"line":188},[137,85078,79757],{"class":147},[137,85080,84895],{"class":284},[137,85082,84898],{"class":284},[137,85084,84901],{"class":284},[137,85086,85087],{"class":284}," OPENAI_API_KEY",[137,85089,84907],{"class":364},[137,85091,85092],{"class":284}," \"sk-xxxxxxxxxxxx...\"\n",[104,85094,85096],{"id":85095},"anatomy-of-a-workflow-file","Anatomy of a Workflow File",[27,85098,85099],{},"Now let's look at how to actually write a workflow. Here's a complete example:",[128,85101,85103],{"className":70935,"code":85102,"language":57972,"meta":133,"style":133},"---\non:\n    issues:\n        types: [opened, reopened]\n\npermissions:\n    contents: read\n    issues: read\n    pull-requests: read\n\nsafe-outputs:\n    add-comment: {}\n    add-labels:\n        allowed: [bug, feature, enhancement, question]\n\ntools:\n    github:\n---\n\n# Issue Triage\n\nWhen a new issue is opened, analyze its content.\n\n## What to do\n\n1. Read the issue title and body\n2. Research the codebase for context\n3. Add the most appropriate label\n4. Leave a friendly comment explaining your choice\n",[22,85104,85105,85109,85114,85119,85124,85128,85133,85138,85143,85148,85152,85157,85162,85167,85172,85176,85181,85186,85190,85194,85199,85203,85208,85212,85217,85221,85226,85231,85236],{"__ignoreMap":133},[137,85106,85107],{"class":139,"line":140},[137,85108,70943],{},[137,85110,85111],{"class":139,"line":173},[137,85112,85113],{},"on:\n",[137,85115,85116],{"class":139,"line":188},[137,85117,85118],{},"    issues:\n",[137,85120,85121],{"class":139,"line":269},[137,85122,85123],{},"        types: [opened, reopened]\n",[137,85125,85126],{"class":139,"line":278},[137,85127,516],{"emptyLinePlaceholder":515},[137,85129,85130],{"class":139,"line":291},[137,85131,85132],{},"permissions:\n",[137,85134,85135],{"class":139,"line":297},[137,85136,85137],{},"    contents: read\n",[137,85139,85140],{"class":139,"line":302},[137,85141,85142],{},"    issues: read\n",[137,85144,85145],{"class":139,"line":662},[137,85146,85147],{},"    pull-requests: read\n",[137,85149,85150],{"class":139,"line":667},[137,85151,516],{"emptyLinePlaceholder":515},[137,85153,85154],{"class":139,"line":786},[137,85155,85156],{},"safe-outputs:\n",[137,85158,85159],{"class":139,"line":798},[137,85160,85161],{},"    add-comment: {}\n",[137,85163,85164],{"class":139,"line":803},[137,85165,85166],{},"    add-labels:\n",[137,85168,85169],{"class":139,"line":931},[137,85170,85171],{},"        allowed: [bug, feature, enhancement, question]\n",[137,85173,85174],{"class":139,"line":1568},[137,85175,516],{"emptyLinePlaceholder":515},[137,85177,85178],{"class":139,"line":1573},[137,85179,85180],{},"tools:\n",[137,85182,85183],{"class":139,"line":1578},[137,85184,85185],{},"    github:\n",[137,85187,85188],{"class":139,"line":1588},[137,85189,70943],{},[137,85191,85192],{"class":139,"line":1601},[137,85193,516],{"emptyLinePlaceholder":515},[137,85195,85196],{"class":139,"line":3802},[137,85197,85198],{},"# Issue Triage\n",[137,85200,85201],{"class":139,"line":3808},[137,85202,516],{"emptyLinePlaceholder":515},[137,85204,85205],{"class":139,"line":3822},[137,85206,85207],{},"When a new issue is opened, analyze its content.\n",[137,85209,85210],{"class":139,"line":3827},[137,85211,516],{"emptyLinePlaceholder":515},[137,85213,85214],{"class":139,"line":3832},[137,85215,85216],{},"## What to do\n",[137,85218,85219],{"class":139,"line":3840},[137,85220,516],{"emptyLinePlaceholder":515},[137,85222,85223],{"class":139,"line":3846},[137,85224,85225],{},"1. Read the issue title and body\n",[137,85227,85228],{"class":139,"line":3861},[137,85229,85230],{},"2. Research the codebase for context\n",[137,85232,85233],{"class":139,"line":3883},[137,85234,85235],{},"3. Add the most appropriate label\n",[137,85237,85238],{"class":139,"line":3896},[137,85239,85240],{},"4. Leave a friendly comment explaining your choice\n",[27,85242,85243],{},"Let's break down each frontmatter property.",[123,85245,4737,85247,85250],{"id":85246},"the-on-property-triggers",[22,85248,85249],{},"on:"," Property - Triggers",[27,85252,4737,85253,85255,85256,85259],{},[22,85254,85249],{}," property defines ",[42,85257,85258],{},"when"," your workflow runs. It uses the same trigger syntax as regular GitHub Actions:",[128,85261,85263],{"className":76224,"code":85262,"language":76226,"meta":133,"style":133},"# Run when an issue is opened or reopened\non:\n  issues:\n    types: [opened, reopened]\n\n# Run on push to main\non:\n  push:\n    branches: [main]\n    paths:\n      - \"content\u002F**\"\n\n# Run on a schedule\non:\n  schedule: daily\n  # or with cron syntax\n  schedule:\n    - cron: \"0 0 * * *\"\n\n# Run when someone comments on an issue\u002FPR (for slash commands)\non:\n  issue_comment:\n    types: [created]\n\n# Run on PR events\non:\n  pull_request:\n    types: [opened, synchronize]\n\n# Allow manual triggering\non:\n  workflow_dispatch:\n",[22,85264,85265,85270,85276,85283,85300,85304,85309,85315,85322,85333,85340,85348,85352,85357,85363,85373,85378,85384,85395,85399,85404,85410,85417,85428,85432,85437,85443,85450,85465,85469,85474,85480],{"__ignoreMap":133},[137,85266,85267],{"class":139,"line":140},[137,85268,85269],{"class":308},"# Run when an issue is opened or reopened\n",[137,85271,85272,85274],{"class":139,"line":173},[137,85273,72117],{"class":364},[137,85275,36830],{"class":157},[137,85277,85278,85281],{"class":139,"line":188},[137,85279,85280],{"class":4036},"  issues",[137,85282,36830],{"class":157},[137,85284,85285,85288,85290,85293,85295,85298],{"class":139,"line":269},[137,85286,85287],{"class":4036},"    types",[137,85289,29669],{"class":157},[137,85291,85292],{"class":284},"opened",[137,85294,164],{"class":157},[137,85296,85297],{"class":284},"reopened",[137,85299,33307],{"class":157},[137,85301,85302],{"class":139,"line":278},[137,85303,516],{"emptyLinePlaceholder":515},[137,85305,85306],{"class":139,"line":291},[137,85307,85308],{"class":308},"# Run on push to main\n",[137,85310,85311,85313],{"class":139,"line":297},[137,85312,72117],{"class":364},[137,85314,36830],{"class":157},[137,85316,85317,85320],{"class":139,"line":302},[137,85318,85319],{"class":4036},"  push",[137,85321,36830],{"class":157},[137,85323,85324,85327,85329,85331],{"class":139,"line":662},[137,85325,85326],{"class":4036},"    branches",[137,85328,29669],{"class":157},[137,85330,58927],{"class":284},[137,85332,33307],{"class":157},[137,85334,85335,85338],{"class":139,"line":667},[137,85336,85337],{"class":4036},"    paths",[137,85339,36830],{"class":157},[137,85341,85342,85345],{"class":139,"line":786},[137,85343,85344],{"class":157},"      - ",[137,85346,85347],{"class":284},"\"content\u002F**\"\n",[137,85349,85350],{"class":139,"line":798},[137,85351,516],{"emptyLinePlaceholder":515},[137,85353,85354],{"class":139,"line":803},[137,85355,85356],{"class":308},"# Run on a schedule\n",[137,85358,85359,85361],{"class":139,"line":931},[137,85360,72117],{"class":364},[137,85362,36830],{"class":157},[137,85364,85365,85368,85370],{"class":139,"line":1568},[137,85366,85367],{"class":4036},"  schedule",[137,85369,726],{"class":157},[137,85371,85372],{"class":284},"daily\n",[137,85374,85375],{"class":139,"line":1573},[137,85376,85377],{"class":308},"  # or with cron syntax\n",[137,85379,85380,85382],{"class":139,"line":1578},[137,85381,85367],{"class":4036},[137,85383,36830],{"class":157},[137,85385,85386,85389,85391,85393],{"class":139,"line":1588},[137,85387,85388],{"class":157},"    - ",[137,85390,76647],{"class":4036},[137,85392,726],{"class":157},[137,85394,76652],{"class":284},[137,85396,85397],{"class":139,"line":1601},[137,85398,516],{"emptyLinePlaceholder":515},[137,85400,85401],{"class":139,"line":3802},[137,85402,85403],{"class":308},"# Run when someone comments on an issue\u002FPR (for slash commands)\n",[137,85405,85406,85408],{"class":139,"line":3808},[137,85407,72117],{"class":364},[137,85409,36830],{"class":157},[137,85411,85412,85415],{"class":139,"line":3822},[137,85413,85414],{"class":4036},"  issue_comment",[137,85416,36830],{"class":157},[137,85418,85419,85421,85423,85426],{"class":139,"line":3827},[137,85420,85287],{"class":4036},[137,85422,29669],{"class":157},[137,85424,85425],{"class":284},"created",[137,85427,33307],{"class":157},[137,85429,85430],{"class":139,"line":3832},[137,85431,516],{"emptyLinePlaceholder":515},[137,85433,85434],{"class":139,"line":3840},[137,85435,85436],{"class":308},"# Run on PR events\n",[137,85438,85439,85441],{"class":139,"line":3846},[137,85440,72117],{"class":364},[137,85442,36830],{"class":157},[137,85444,85445,85448],{"class":139,"line":3861},[137,85446,85447],{"class":4036},"  pull_request",[137,85449,36830],{"class":157},[137,85451,85452,85454,85456,85458,85460,85463],{"class":139,"line":3883},[137,85453,85287],{"class":4036},[137,85455,29669],{"class":157},[137,85457,85292],{"class":284},[137,85459,164],{"class":157},[137,85461,85462],{"class":284},"synchronize",[137,85464,33307],{"class":157},[137,85466,85467],{"class":139,"line":3896},[137,85468,516],{"emptyLinePlaceholder":515},[137,85470,85471],{"class":139,"line":3901},[137,85472,85473],{"class":308},"# Allow manual triggering\n",[137,85475,85476,85478],{"class":139,"line":3906},[137,85477,72117],{"class":364},[137,85479,36830],{"class":157},[137,85481,85482,85485],{"class":139,"line":3911},[137,85483,85484],{"class":4036},"  workflow_dispatch",[137,85486,36830],{"class":157},[27,85488,85489],{},"You can combine multiple triggers. For example, a workflow that runs weekly but can also be triggered manually:",[128,85491,85493],{"className":76224,"code":85492,"language":76226,"meta":133,"style":133},"on:\n    schedule: weekly\n    workflow_dispatch:\n",[22,85494,85495,85501,85510],{"__ignoreMap":133},[137,85496,85497,85499],{"class":139,"line":140},[137,85498,72117],{"class":364},[137,85500,36830],{"class":157},[137,85502,85503,85505,85507],{"class":139,"line":173},[137,85504,76632],{"class":4036},[137,85506,726],{"class":157},[137,85508,85509],{"class":284},"weekly\n",[137,85511,85512,85514],{"class":139,"line":188},[137,85513,76678],{"class":4036},[137,85515,36830],{"class":157},[123,85517,4737,85519,85522],{"id":85518},"the-permissions-property",[22,85520,85521],{},"permissions:"," Property",[27,85524,4737,85525,85527,85528,85531],{},[22,85526,85521],{}," property defines what the AI agent can ",[42,85529,85530],{},"read",". Agentic workflows are read-only by default - write permissions are not allowed (the compiler will reject them). All write operations go through safe outputs instead.",[128,85533,85535],{"className":76224,"code":85534,"language":76226,"meta":133,"style":133},"permissions:\n    contents: read\n    issues: read\n    pull-requests: read\n    actions: read\n",[22,85536,85537,85543,85553,85562,85571],{"__ignoreMap":133},[137,85538,85539,85541],{"class":139,"line":140},[137,85540,14019],{"class":4036},[137,85542,36830],{"class":157},[137,85544,85545,85548,85550],{"class":139,"line":173},[137,85546,85547],{"class":4036},"    contents",[137,85549,726],{"class":157},[137,85551,85552],{"class":284},"read\n",[137,85554,85555,85558,85560],{"class":139,"line":188},[137,85556,85557],{"class":4036},"    issues",[137,85559,726],{"class":157},[137,85561,85552],{"class":284},[137,85563,85564,85567,85569],{"class":139,"line":269},[137,85565,85566],{"class":4036},"    pull-requests",[137,85568,726],{"class":157},[137,85570,85552],{"class":284},[137,85572,85573,85576,85578],{"class":139,"line":278},[137,85574,85575],{"class":4036},"    actions",[137,85577,726],{"class":157},[137,85579,85552],{"class":284},[27,85581,85582,85583,164,85586,164,85589,164,85592,164,85595,164,85598,164,85601,164,85604,164,85607,14528,85610,85613],{},"Available permissions include ",[22,85584,85585],{},"contents",[22,85587,85588],{},"issues",[22,85590,85591],{},"pull-requests",[22,85593,85594],{},"actions",[22,85596,85597],{},"discussions",[22,85599,85600],{},"checks",[22,85602,85603],{},"deployments",[22,85605,85606],{},"packages",[22,85608,85609],{},"pages",[22,85611,85612],{},"statuses"," - all read-only.",[3244,85615,85616],{},[27,85617,85618,85619,85622,85623,164,85626,14528,85629,85632,85633,164,85636,14528,85638,1017],{},"When you use ",[22,85620,85621],{},"tools: github:"," without specifying toolsets, the compiler expects at minimum ",[22,85624,85625],{},"contents: read",[22,85627,85628],{},"issues: read",[22,85630,85631],{},"pull-requests: read",", since the default GitHub toolsets include ",[22,85634,85635],{},"repos",[22,85637,85588],{},[22,85639,85640],{},"pull_requests",[123,85642,4737,85644,85522],{"id":85643},"the-engine-property",[22,85645,84986],{},[27,85647,4737,85648,85650,85651,1017],{},[22,85649,84986],{}," property selects which AI model interprets your workflow. If omitted, it defaults to ",[42,85652,84765],{},[128,85654,85656],{"className":76224,"code":85655,"language":76226,"meta":133,"style":133},"# Default - GitHub Copilot\nengine: copilot\n\n# With a specific model\nengine:\n  id: copilot\n  model: gpt-5.2-codex\n\n# Claude by Anthropic\nengine: claude\n\n# OpenAI Codex\nengine: codex\n",[22,85657,85658,85663,85672,85676,85681,85687,85696,85706,85710,85715,85724,85728,85733],{"__ignoreMap":133},[137,85659,85660],{"class":139,"line":140},[137,85661,85662],{"class":308},"# Default - GitHub Copilot\n",[137,85664,85665,85667,85669],{"class":139,"line":173},[137,85666,84630],{"class":4036},[137,85668,726],{"class":157},[137,85670,85671],{"class":284},"copilot\n",[137,85673,85674],{"class":139,"line":188},[137,85675,516],{"emptyLinePlaceholder":515},[137,85677,85678],{"class":139,"line":269},[137,85679,85680],{"class":308},"# With a specific model\n",[137,85682,85683,85685],{"class":139,"line":278},[137,85684,84630],{"class":4036},[137,85686,36830],{"class":157},[137,85688,85689,85692,85694],{"class":139,"line":291},[137,85690,85691],{"class":4036},"  id",[137,85693,726],{"class":157},[137,85695,85671],{"class":284},[137,85697,85698,85701,85703],{"class":139,"line":297},[137,85699,85700],{"class":4036},"  model",[137,85702,726],{"class":157},[137,85704,85705],{"class":284},"gpt-5.2-codex\n",[137,85707,85708],{"class":139,"line":302},[137,85709,516],{"emptyLinePlaceholder":515},[137,85711,85712],{"class":139,"line":662},[137,85713,85714],{"class":308},"# Claude by Anthropic\n",[137,85716,85717,85719,85721],{"class":139,"line":667},[137,85718,84630],{"class":4036},[137,85720,726],{"class":157},[137,85722,85723],{"class":284},"claude\n",[137,85725,85726],{"class":139,"line":786},[137,85727,516],{"emptyLinePlaceholder":515},[137,85729,85730],{"class":139,"line":798},[137,85731,85732],{"class":308},"# OpenAI Codex\n",[137,85734,85735,85737,85739],{"class":139,"line":803},[137,85736,84630],{"class":4036},[137,85738,726],{"class":157},[137,85740,85741],{"class":284},"codex\n",[27,85743,85744,85745,85748],{},"Claude and Codex are currently marked as ",[42,85746,85747],{},"experimental",". Copilot is the most stable option.",[123,85750,4737,85752,85522],{"id":85751},"the-tools-property",[22,85753,85754],{},"tools:",[27,85756,4737,85757,85759,85760,85763],{},[22,85758,85754],{}," property declares which ",[42,85761,85762],{},"MCP tools"," the AI agent can use. Each tool must be explicitly declared - the agent can't access anything you don't allow.",[128,85765,85767],{"className":76224,"code":85766,"language":76226,"meta":133,"style":133},"tools:\n    github:\n",[22,85768,85769,85775],{"__ignoreMap":133},[137,85770,85771,85773],{"class":139,"line":140},[137,85772,84637],{"class":4036},[137,85774,36830],{"class":157},[137,85776,85777,85780],{"class":139,"line":173},[137,85778,85779],{"class":4036},"    github",[137,85781,36830],{"class":157},[27,85783,85784,85785,164,85787,164,85789,164,85791,14528,85793,85796],{},"This gives the agent access to the GitHub API. Without specifying toolsets, it enables the defaults: ",[22,85786,14097],{},[22,85788,85635],{},[22,85790,85588],{},[22,85792,85640],{},[22,85794,85795],{},"users",". You can restrict it:",[128,85798,85800],{"className":76224,"code":85799,"language":76226,"meta":133,"style":133},"tools:\n    github:\n        toolsets: [issues, repos]\n",[22,85801,85802,85808,85814],{"__ignoreMap":133},[137,85803,85804,85806],{"class":139,"line":140},[137,85805,84637],{"class":4036},[137,85807,36830],{"class":157},[137,85809,85810,85812],{"class":139,"line":173},[137,85811,85779],{"class":4036},[137,85813,36830],{"class":157},[137,85815,85816,85819,85821,85823,85825,85827],{"class":139,"line":188},[137,85817,85818],{"class":4036},"        toolsets",[137,85820,29669],{"class":157},[137,85822,85588],{"class":284},[137,85824,164],{"class":157},[137,85826,85635],{"class":284},[137,85828,33307],{"class":157},[27,85830,85831],{},"Here are all the available tools:",[27,85833,85834],{},[42,85835,85836],{},"Core tools:",[45740,85838,85839,85848],{},[45743,85840,85841],{},[45746,85842,85843,85846],{},[45749,85844,85845],{},"Tool",[45749,85847,56463],{},[45762,85849,85850,85860,85870],{},[45746,85851,85852,85857],{},[45767,85853,85854],{},[22,85855,85856],{},"github:",[45767,85858,85859],{},"GitHub API access (repos, issues, PRs, actions, labels, search, etc.)",[45746,85861,85862,85867],{},[45767,85863,85864],{},[22,85865,85866],{},"bash:",[45767,85868,85869],{},"Shell command execution (safe commands by default)",[45746,85871,85872,85877],{},[45767,85873,85874],{},[22,85875,85876],{},"edit:",[45767,85878,85879],{},"File editing in the workspace",[27,85881,85882],{},[42,85883,85884],{},"Web tools:",[45740,85886,85887,85895],{},[45743,85888,85889],{},[45746,85890,85891,85893],{},[45749,85892,85845],{},[45749,85894,56463],{},[45762,85896,85897,85907,85917],{},[45746,85898,85899,85904],{},[45767,85900,85901],{},[22,85902,85903],{},"web-fetch:",[45767,85905,85906],{},"HTTP requests to fetch web content",[45746,85908,85909,85914],{},[45767,85910,85911],{},[22,85912,85913],{},"web-search:",[45767,85915,85916],{},"Web search capabilities",[45746,85918,85919,85924],{},[45767,85920,85921],{},[22,85922,85923],{},"playwright:",[45767,85925,85926],{},"Browser automation with domain-based access control",[27,85928,85929],{},[42,85930,85931],{},"Built-in MCP tools:",[45740,85933,85934,85942],{},[45743,85935,85936],{},[45746,85937,85938,85940],{},[45749,85939,85845],{},[45749,85941,56463],{},[45762,85943,85944,85954,85964],{},[45746,85945,85946,85951],{},[45767,85947,85948],{},[22,85949,85950],{},"agentic-workflows:",[45767,85952,85953],{},"Workflow introspection, log analysis, and debugging",[45746,85955,85956,85961],{},[45767,85957,85958],{},[22,85959,85960],{},"cache-memory:",[45767,85962,85963],{},"Persistent memory storage across workflow runs",[45746,85965,85966,85971],{},[45767,85967,85968],{},[22,85969,85970],{},"repo-memory:",[45767,85972,85973],{},"Repository-specific memory across executions",[27,85975,85976,85977,85980],{},"You can also integrate custom MCP servers using the ",[22,85978,85979],{},"mcp-servers:"," top-level frontmatter field:",[128,85982,85984],{"className":76224,"code":85983,"language":76226,"meta":133,"style":133},"mcp-servers:\n    slack:\n        command: \"npx\"\n        args: [\"-y\", \"@slack\u002Fmcp-server\"]\n        env:\n            SLACK_BOT_TOKEN: \"${{ secrets.SLACK_BOT_TOKEN }}\"\n        allowed: [\"send_message\", \"get_channel_history\"]\n",[22,85985,85986,85993,86000,86010,86027,86034,86044],{"__ignoreMap":133},[137,85987,85988,85991],{"class":139,"line":140},[137,85989,85990],{"class":4036},"mcp-servers",[137,85992,36830],{"class":157},[137,85994,85995,85998],{"class":139,"line":173},[137,85996,85997],{"class":4036},"    slack",[137,85999,36830],{"class":157},[137,86001,86002,86005,86007],{"class":139,"line":188},[137,86003,86004],{"class":4036},"        command",[137,86006,726],{"class":157},[137,86008,86009],{"class":284},"\"npx\"\n",[137,86011,86012,86015,86017,86020,86022,86025],{"class":139,"line":269},[137,86013,86014],{"class":4036},"        args",[137,86016,29669],{"class":157},[137,86018,86019],{"class":284},"\"-y\"",[137,86021,164],{"class":157},[137,86023,86024],{"class":284},"\"@slack\u002Fmcp-server\"",[137,86026,33307],{"class":157},[137,86028,86029,86032],{"class":139,"line":278},[137,86030,86031],{"class":4036},"        env",[137,86033,36830],{"class":157},[137,86035,86036,86039,86041],{"class":139,"line":291},[137,86037,86038],{"class":4036},"            SLACK_BOT_TOKEN",[137,86040,726],{"class":157},[137,86042,86043],{"class":284},"\"${{ secrets.SLACK_BOT_TOKEN }}\"\n",[137,86045,86046,86049,86051,86054,86056,86059],{"class":139,"line":297},[137,86047,86048],{"class":4036},"        allowed",[137,86050,29669],{"class":157},[137,86052,86053],{"class":284},"\"send_message\"",[137,86055,164],{"class":157},[137,86057,86058],{"class":284},"\"get_channel_history\"",[137,86060,33307],{"class":157},[123,86062,4737,86064,85522],{"id":86063},"the-safe-outputs-property",[22,86065,86066],{},"safe-outputs:",[27,86068,86069,86070,86073],{},"This is the most important security concept in agentic workflows. Since the agent is read-only, every write operation must go through a ",[42,86071,86072],{},"safe output"," - a pre-approved, reviewable GitHub operation.",[128,86075,86077],{"className":76224,"code":86076,"language":76226,"meta":133,"style":133},"safe-outputs:\n    create-issue:\n        title-prefix: \"[Weekly Report] \"\n        labels: [report]\n        group: true\n        expires: 14\n    create-pull-request:\n        title-prefix: \"[docs] \"\n        labels: [documentation]\n    add-comment: {}\n    add-labels:\n        allowed: [bug, feature, enhancement, question]\n",[22,86078,86079,86086,86093,86103,86115,86124,86134,86141,86150,86160,86168,86175],{"__ignoreMap":133},[137,86080,86081,86084],{"class":139,"line":140},[137,86082,86083],{"class":4036},"safe-outputs",[137,86085,36830],{"class":157},[137,86087,86088,86091],{"class":139,"line":173},[137,86089,86090],{"class":4036},"    create-issue",[137,86092,36830],{"class":157},[137,86094,86095,86098,86100],{"class":139,"line":188},[137,86096,86097],{"class":4036},"        title-prefix",[137,86099,726],{"class":157},[137,86101,86102],{"class":284},"\"[Weekly Report] \"\n",[137,86104,86105,86108,86110,86113],{"class":139,"line":269},[137,86106,86107],{"class":4036},"        labels",[137,86109,29669],{"class":157},[137,86111,86112],{"class":284},"report",[137,86114,33307],{"class":157},[137,86116,86117,86120,86122],{"class":139,"line":278},[137,86118,86119],{"class":4036},"        group",[137,86121,726],{"class":157},[137,86123,78248],{"class":364},[137,86125,86126,86129,86131],{"class":139,"line":291},[137,86127,86128],{"class":4036},"        expires",[137,86130,726],{"class":157},[137,86132,86133],{"class":364},"14\n",[137,86135,86136,86139],{"class":139,"line":297},[137,86137,86138],{"class":4036},"    create-pull-request",[137,86140,36830],{"class":157},[137,86142,86143,86145,86147],{"class":139,"line":302},[137,86144,86097],{"class":4036},[137,86146,726],{"class":157},[137,86148,86149],{"class":284},"\"[docs] \"\n",[137,86151,86152,86154,86156,86158],{"class":139,"line":662},[137,86153,86107],{"class":4036},[137,86155,29669],{"class":157},[137,86157,31313],{"class":284},[137,86159,33307],{"class":157},[137,86161,86162,86165],{"class":139,"line":667},[137,86163,86164],{"class":4036},"    add-comment",[137,86166,86167],{"class":157},": {}\n",[137,86169,86170,86173],{"class":139,"line":786},[137,86171,86172],{"class":4036},"    add-labels",[137,86174,36830],{"class":157},[137,86176,86177,86179,86181,86184,86186,86189,86191,86194,86196,86199],{"class":139,"line":798},[137,86178,86048],{"class":4036},[137,86180,29669],{"class":157},[137,86182,86183],{"class":284},"bug",[137,86185,164],{"class":157},[137,86187,86188],{"class":284},"feature",[137,86190,164],{"class":157},[137,86192,86193],{"class":284},"enhancement",[137,86195,164],{"class":157},[137,86197,86198],{"class":284},"question",[137,86200,33307],{"class":157},[27,86202,86203],{},"Available safe-output types:",[45740,86205,86206,86218],{},[45743,86207,86208],{},[45746,86209,86210,86213,86215],{},[45749,86211,86212],{},"Type",[45749,86214,56463],{},[45749,86216,86217],{},"Configuration Options",[45762,86219,86220,86244,86260,86276,86291],{},[45746,86221,86222,86227,86230],{},[45767,86223,86224],{},[22,86225,86226],{},"create-issue",[45767,86228,86229],{},"Create a new GitHub issue",[45767,86231,86232,164,86235,164,86238,164,86241],{},[22,86233,86234],{},"title-prefix",[22,86236,86237],{},"labels",[22,86239,86240],{},"group",[22,86242,86243],{},"expires",[45746,86245,86246,86251,86254],{},[45767,86247,86248],{},[22,86249,86250],{},"create-pull-request",[45767,86252,86253],{},"Create a new pull request",[45767,86255,86256,164,86258],{},[22,86257,86234],{},[22,86259,86237],{},[45746,86261,86262,86267,86270],{},[45767,86263,86264],{},[22,86265,86266],{},"add-comment",[45767,86268,86269],{},"Add a comment to an issue or PR",[45767,86271,86272,86275],{},[22,86273,86274],{},"{}"," (no configuration needed)",[45746,86277,86278,86283,86286],{},[45767,86279,86280],{},[22,86281,86282],{},"add-labels",[45767,86284,86285],{},"Add labels to an issue or PR",[45767,86287,86288,86290],{},[22,86289,65424],{}," (list of permitted labels)",[45746,86292,86293,86298,86301],{},[45767,86294,86295],{},[22,86296,86297],{},"missing-data",[45767,86299,86300],{},"Report when the agent lacks data to complete a task",[45767,86302,86303],{},"Encourages honest AI behaviour",[27,86305,4737,86306,86309,86310,86313],{},[22,86307,86308],{},"group: true"," option ensures only one open issue exists at a time for that workflow (new runs update the existing issue rather than creating duplicates). The ",[22,86311,86312],{},"expires: 14"," option auto-closes the issue after 14 days.",[123,86315,4737,86317,85522],{"id":86316},"the-network-property",[22,86318,86319],{},"network:",[27,86321,4737,86322,86324],{},[22,86323,86319],{}," property controls outbound network access for the agent:",[128,86326,86328],{"className":76224,"code":86327,"language":76226,"meta":133,"style":133},"network: defaults\n",[22,86329,86330],{"__ignoreMap":133},[137,86331,86332,86335,86337],{"class":139,"line":140},[137,86333,86334],{"class":4036},"network",[137,86336,726],{"class":157},[137,86338,86339],{"class":284},"defaults\n",[27,86341,86342,86343,86346],{},"When enabled, the ",[42,86344,86345],{},"Agent Workflow Firewall (AWF)"," wraps execution and enforces domain-based allowlists, logging all network activity for audit purposes.",[123,86348,86350],{"id":86349},"the-markdown-body","The Markdown Body",[27,86352,86353],{},"After the frontmatter, the rest of the file is plain Markdown. This is where you write natural language instructions for the AI agent:",[128,86355,86357],{"className":70935,"code":86356,"language":57972,"meta":133,"style":133},"# Issue Triage\n\nWhen a new issue is opened, analyze its content.\n\n## What to do\n\n1. Read the issue title and body\n2. Research the codebase for context\n3. Add the most appropriate label\n4. Leave a friendly comment explaining your choice\n\n## Tone\n\nBe friendly and professional. Thank the person for opening the issue.\n",[22,86358,86359,86363,86367,86371,86375,86379,86383,86387,86391,86395,86399,86403,86408,86412],{"__ignoreMap":133},[137,86360,86361],{"class":139,"line":140},[137,86362,85198],{},[137,86364,86365],{"class":139,"line":173},[137,86366,516],{"emptyLinePlaceholder":515},[137,86368,86369],{"class":139,"line":188},[137,86370,85207],{},[137,86372,86373],{"class":139,"line":269},[137,86374,516],{"emptyLinePlaceholder":515},[137,86376,86377],{"class":139,"line":278},[137,86378,85216],{},[137,86380,86381],{"class":139,"line":291},[137,86382,516],{"emptyLinePlaceholder":515},[137,86384,86385],{"class":139,"line":297},[137,86386,85225],{},[137,86388,86389],{"class":139,"line":302},[137,86390,85230],{},[137,86392,86393],{"class":139,"line":662},[137,86394,85235],{},[137,86396,86397],{"class":139,"line":667},[137,86398,85240],{},[137,86400,86401],{"class":139,"line":786},[137,86402,516],{"emptyLinePlaceholder":515},[137,86404,86405],{"class":139,"line":798},[137,86406,86407],{},"## Tone\n",[137,86409,86410],{"class":139,"line":803},[137,86411,516],{"emptyLinePlaceholder":515},[137,86413,86414],{"class":139,"line":931},[137,86415,86416],{},"Be friendly and professional. Thank the person for opening the issue.\n",[27,86418,86419],{},"The agent interprets these instructions flexibly. You don't need to be overly prescriptive - the agent understands context and makes judgement calls. But the more specific your instructions, the more consistent the results.",[104,86421,86423],{"id":86422},"compiling-and-running-workflows","Compiling and Running Workflows",[123,86425,86427,86428,12972],{"id":86426},"what-is-gh-aw-compile","What Is ",[22,86429,86430],{},"gh aw compile",[27,86432,86433,86434,86436,86437,86439],{},"Once you've written your ",[22,86435,80806],{}," workflow file, you need to ",[42,86438,84750],{}," it:",[128,86441,86443],{"className":8665,"code":86442,"language":8667,"meta":133,"style":133},"gh aw compile\n",[22,86444,86445],{"__ignoreMap":133},[137,86446,86447,86449,86451],{"class":139,"line":140},[137,86448,79757],{"class":147},[137,86450,84895],{"class":284},[137,86452,86453],{"class":284}," compile\n",[27,86455,86456,86457,86459,86460,86462],{},"This takes your ",[22,86458,80806],{}," file and generates a corresponding ",[22,86461,84583],{}," file - a standard GitHub Actions YAML workflow with security hardening.",[27,86464,86465],{},"Think of it like a build step:",[1003,86467,86468,86477],{},[1006,86469,86470,3596,86473,86476],{},[42,86471,86472],{},"Input:",[22,86474,86475],{},".github\u002Fworkflows\u002Fmy-workflow.md"," (your Markdown source)",[1006,86478,86479,3596,86482,86485],{},[42,86480,86481],{},"Output:",[22,86483,86484],{},".github\u002Fworkflows\u002Fmy-workflow.lock.yml"," (compiled Actions YAML)",[27,86487,86488,86489,86491,86492,86494],{},"The compiled file includes SHA-pinned action dependencies, sandbox configuration, safe-output job separation, and security hardening. GitHub Actions executes the ",[22,86490,84583],{},", but reads the Markdown instructions from the ",[22,86493,80806],{}," at runtime.",[27,86496,86497,86500],{},[42,86498,86499],{},"You only need to recompile when you change the frontmatter"," (triggers, permissions, tools, safe-outputs). If you only edit the Markdown body, no recompilation is needed.",[123,86502,4737,86504,86506],{"id":86503},"the-lockyml-files",[22,86505,84583],{}," Files",[27,86508,4737,86509,86511,86512,114,86514,86516],{},[22,86510,84583],{}," files are the compiled output. They're typically 50+ KB of generated YAML that you don't need to read or edit. Both the ",[22,86513,80806],{},[22,86515,84583],{}," files must be committed and pushed for the workflow to run.",[123,86518,4737,86520,86523],{"id":86519},"the-actions-lockjson-file",[22,86521,86522],{},"actions-lock.json"," File",[27,86525,86526,86527,86530,86531,894],{},"When you compile, you'll also notice a ",[22,86528,86529],{},".github\u002Faw\u002Factions-lock.json"," file gets created. This pins every GitHub Action used in your compiled workflows to a ",[42,86532,86533],{},"specific commit SHA",[128,86535,86537],{"className":5155,"code":86536,"language":5157,"meta":133,"style":133},"{\n    \"entries\": {\n        \"actions\u002Fgithub-script@v8\": {\n            \"repo\": \"actions\u002Fgithub-script\",\n            \"version\": \"v8\",\n            \"sha\": \"ed597411d8f924073f98dfc5c65a23a2325f34cd\"\n        }\n    }\n}\n",[22,86538,86539,86543,86550,86557,86569,86581,86591,86595,86599],{"__ignoreMap":133},[137,86540,86541],{"class":139,"line":140},[137,86542,15971],{"class":157},[137,86544,86545,86548],{"class":139,"line":173},[137,86546,86547],{"class":364},"    \"entries\"",[137,86549,1819],{"class":157},[137,86551,86552,86555],{"class":139,"line":188},[137,86553,86554],{"class":364},"        \"actions\u002Fgithub-script@v8\"",[137,86556,1819],{"class":157},[137,86558,86559,86562,86564,86567],{"class":139,"line":269},[137,86560,86561],{"class":364},"            \"repo\"",[137,86563,726],{"class":157},[137,86565,86566],{"class":284},"\"actions\u002Fgithub-script\"",[137,86568,1961],{"class":157},[137,86570,86571,86574,86576,86579],{"class":139,"line":278},[137,86572,86573],{"class":364},"            \"version\"",[137,86575,726],{"class":157},[137,86577,86578],{"class":284},"\"v8\"",[137,86580,1961],{"class":157},[137,86582,86583,86586,86588],{"class":139,"line":291},[137,86584,86585],{"class":364},"            \"sha\"",[137,86587,726],{"class":157},[137,86589,86590],{"class":284},"\"ed597411d8f924073f98dfc5c65a23a2325f34cd\"\n",[137,86592,86593],{"class":139,"line":297},[137,86594,1966],{"class":157},[137,86596,86597],{"class":139,"line":302},[137,86598,294],{"class":157},[137,86600,86601],{"class":139,"line":662},[137,86602,510],{"class":157},[27,86604,86605,86606,86609,86610,86613,86614,3955,86617,86620],{},"Instead of referencing ",[22,86607,86608],{},"actions\u002Fgithub-script@v8"," (a mutable tag), the compiled workflows use the exact SHA. This prevents supply chain attacks - even if someone compromises the ",[22,86611,86612],{},"v8"," tag, your workflows use the verified version. It's the same concept as ",[22,86615,86616],{},"yarn.lock",[22,86618,86619],{},"package-lock.json",", but for GitHub Actions.",[27,86622,86623],{},"Commit this file alongside everything else.",[123,86625,4737,86627,86523],{"id":86626},"the-agentics-maintenanceyml-file",[22,86628,86629],{},"agentics-maintenance.yml",[27,86631,86632,86633,86636,86637,86639,86640,86642,86643,86646],{},"If any of your workflows use the ",[22,86634,86635],{},"expires:"," field in safe-outputs, ",[22,86638,86430],{}," will also generate an ",[22,86641,86629],{}," file. This is an auto-generated housekeeping workflow that runs daily and ",[42,86644,86645],{},"auto-closes expired issues, discussions, and pull requests"," created by your agentic workflows.",[27,86648,86649,86650,86652],{},"For example, if your weekly report workflow has ",[22,86651,86312],{},", old report issues will be automatically closed after 14 days so they don't pile up. Commit this file too.",[123,86654,4737,86656,86523],{"id":86655},"the-gitattributes-file",[22,86657,86658],{},".gitattributes",[27,86660,86661,86662,86664],{},"The compiler also creates a ",[22,86663,86658],{}," entry:",[128,86666,86669],{"className":86667,"code":86668,"language":5189},[5187],".github\u002Fworkflows\u002F*.lock.yml linguist-generated=true merge=ours\n",[22,86670,86668],{"__ignoreMap":133},[27,86672,86673,86674,86676],{},"This does two things: it tells GitHub to treat ",[22,86675,84583],{}," files as generated (they'll be collapsed in PR diffs), and it resolves merge conflicts by keeping your branch's version (since you'd just recompile anyway).",[123,86678,86680],{"id":86679},"running-workflows","Running Workflows",[27,86682,86683,86684,86687],{},"For workflows with ",[22,86685,86686],{},"workflow_dispatch:"," in their triggers, you can run them manually:",[128,86689,86691],{"className":8665,"code":86690,"language":8667,"meta":133,"style":133},"gh aw run weekly-content-report\n",[22,86692,86693],{"__ignoreMap":133},[137,86694,86695,86697,86699,86701],{"class":139,"line":140},[137,86696,79757],{"class":147},[137,86698,84895],{"class":284},[137,86700,9578],{"class":284},[137,86702,86703],{"class":284}," weekly-content-report\n",[27,86705,86706],{},"This is equivalent to clicking \"Run workflow\" in the GitHub Actions UI. The agent spins up on GitHub's infrastructure, reads your instructions, and produces results in about 2-3 minutes.",[27,86708,86709,86710,86713],{},"You can also trigger this from the ",[42,86711,86712],{},"GitHub Actions tab"," in your browser.",[27,86715,86716,86717,3955,86719,86721],{},"For event-triggered workflows (like ",[22,86718,8583],{},[22,86720,85588],{},"), you trigger them by creating the actual event - pushing code, opening an issue, or commenting on a PR.",[104,86723,86725],{"id":86724},"building-practical-workflows-for-my-blog","Building Practical Workflows for My Blog",[27,86727,86728],{},"Now let's put everything together. I built six agentic workflows for this blog's repository. Each one solves a real problem I've encountered while maintaining a technical blog.",[123,86730,86732],{"id":86731},"workflow-1-article-quality-reviewer","Workflow 1: Article Quality Reviewer",[27,86734,86735,86738],{},[42,86736,86737],{},"Problem:"," When I push a new article, I sometimes miss a frontmatter field, forget to add exactly 3 tags, or write a description that's too short for good SEO.",[27,86740,86741,86744,86745,86748],{},[42,86742,86743],{},"Solution:"," This workflow automatically reviews every new article pushed to ",[22,86746,86747],{},"content\u002F**"," and creates an issue with findings.",[128,86750,86752],{"className":70935,"code":86751,"language":57972,"meta":133,"style":133},"---\non:\n    push:\n        branches: [main]\n        paths:\n            - \"content\u002F**\"\n\npermissions:\n    contents: read\n    issues: read\n    pull-requests: read\n\nsafe-outputs:\n    create-issue:\n        title-prefix: \"[Article Review] \"\n        labels: [content-review]\n\ntools:\n    github:\n---\n\n# Article Quality Reviewer\n\nWhen new or modified blog articles are pushed to the `main` branch,\nreview them for quality and consistency.\n\n## Required Frontmatter Fields\n\nEvery blog article must have ALL of these fields:\n\n- `title` (string)\n- `description` (string) - should be 120-160 characters for optimal SEO\n- `image` (string, valid URL)\n- `keywords` (array of strings) - at least 3\n- `type` (string) - usually `page`\n- `blog` (string) - usually `post`\n- `published` (string) - human-readable format like \"2nd March 2025\"\n- `readTime` (number) - typically between 3 and 20\n- `author` (string) - should be \"Aleksandar Trpkovski\"\n- `articleTags` (array of strings) - exactly 3 tags\n\n## What to Review\n\n1. Identify changed files in `content\u002F` from the most recent push\n2. Validate frontmatter completeness and types\n3. Check SEO quality (description length, keyword count, title length)\n4. Check tag consistency (exactly 3 from established list)\n5. Check external links include `target=\"_blank\"` and `rel=\"noopener noreferrer\"`\n6. Flag very short articles (under 500 words) or very long ones (over 5000 words)\n\n## Output Format\n\nCreate a single issue with findings for each article, including\na quality score: Excellent \u002F Good \u002F Needs Attention.\n",[22,86753,86754,86758,86762,86767,86772,86777,86782,86786,86790,86794,86798,86802,86806,86810,86815,86820,86825,86829,86833,86837,86841,86845,86850,86854,86859,86864,86868,86873,86877,86882,86886,86891,86896,86901,86906,86911,86916,86921,86926,86931,86936,86940,86945,86949,86954,86959,86964,86969,86974,86979,86983,86988,86992,86997],{"__ignoreMap":133},[137,86755,86756],{"class":139,"line":140},[137,86757,70943],{},[137,86759,86760],{"class":139,"line":173},[137,86761,85113],{},[137,86763,86764],{"class":139,"line":188},[137,86765,86766],{},"    push:\n",[137,86768,86769],{"class":139,"line":269},[137,86770,86771],{},"        branches: [main]\n",[137,86773,86774],{"class":139,"line":278},[137,86775,86776],{},"        paths:\n",[137,86778,86779],{"class":139,"line":291},[137,86780,86781],{},"            - \"content\u002F**\"\n",[137,86783,86784],{"class":139,"line":297},[137,86785,516],{"emptyLinePlaceholder":515},[137,86787,86788],{"class":139,"line":302},[137,86789,85132],{},[137,86791,86792],{"class":139,"line":662},[137,86793,85137],{},[137,86795,86796],{"class":139,"line":667},[137,86797,85142],{},[137,86799,86800],{"class":139,"line":786},[137,86801,85147],{},[137,86803,86804],{"class":139,"line":798},[137,86805,516],{"emptyLinePlaceholder":515},[137,86807,86808],{"class":139,"line":803},[137,86809,85156],{},[137,86811,86812],{"class":139,"line":931},[137,86813,86814],{},"    create-issue:\n",[137,86816,86817],{"class":139,"line":1568},[137,86818,86819],{},"        title-prefix: \"[Article Review] \"\n",[137,86821,86822],{"class":139,"line":1573},[137,86823,86824],{},"        labels: [content-review]\n",[137,86826,86827],{"class":139,"line":1578},[137,86828,516],{"emptyLinePlaceholder":515},[137,86830,86831],{"class":139,"line":1588},[137,86832,85180],{},[137,86834,86835],{"class":139,"line":1601},[137,86836,85185],{},[137,86838,86839],{"class":139,"line":3802},[137,86840,70943],{},[137,86842,86843],{"class":139,"line":3808},[137,86844,516],{"emptyLinePlaceholder":515},[137,86846,86847],{"class":139,"line":3822},[137,86848,86849],{},"# Article Quality Reviewer\n",[137,86851,86852],{"class":139,"line":3827},[137,86853,516],{"emptyLinePlaceholder":515},[137,86855,86856],{"class":139,"line":3832},[137,86857,86858],{},"When new or modified blog articles are pushed to the `main` branch,\n",[137,86860,86861],{"class":139,"line":3840},[137,86862,86863],{},"review them for quality and consistency.\n",[137,86865,86866],{"class":139,"line":3846},[137,86867,516],{"emptyLinePlaceholder":515},[137,86869,86870],{"class":139,"line":3861},[137,86871,86872],{},"## Required Frontmatter Fields\n",[137,86874,86875],{"class":139,"line":3883},[137,86876,516],{"emptyLinePlaceholder":515},[137,86878,86879],{"class":139,"line":3896},[137,86880,86881],{},"Every blog article must have ALL of these fields:\n",[137,86883,86884],{"class":139,"line":3901},[137,86885,516],{"emptyLinePlaceholder":515},[137,86887,86888],{"class":139,"line":3906},[137,86889,86890],{},"- `title` (string)\n",[137,86892,86893],{"class":139,"line":3911},[137,86894,86895],{},"- `description` (string) - should be 120-160 characters for optimal SEO\n",[137,86897,86898],{"class":139,"line":4666},[137,86899,86900],{},"- `image` (string, valid URL)\n",[137,86902,86903],{"class":139,"line":4672},[137,86904,86905],{},"- `keywords` (array of strings) - at least 3\n",[137,86907,86908],{"class":139,"line":4680},[137,86909,86910],{},"- `type` (string) - usually `page`\n",[137,86912,86913],{"class":139,"line":4711},[137,86914,86915],{},"- `blog` (string) - usually `post`\n",[137,86917,86918],{"class":139,"line":4716},[137,86919,86920],{},"- `published` (string) - human-readable format like \"2nd March 2025\"\n",[137,86922,86923],{"class":139,"line":4721},[137,86924,86925],{},"- `readTime` (number) - typically between 3 and 20\n",[137,86927,86928],{"class":139,"line":4727},[137,86929,86930],{},"- `author` (string) - should be \"Aleksandar Trpkovski\"\n",[137,86932,86933],{"class":139,"line":4732},[137,86934,86935],{},"- `articleTags` (array of strings) - exactly 3 tags\n",[137,86937,86938],{"class":139,"line":5006},[137,86939,516],{"emptyLinePlaceholder":515},[137,86941,86942],{"class":139,"line":5014},[137,86943,86944],{},"## What to Review\n",[137,86946,86947],{"class":139,"line":14343},[137,86948,516],{"emptyLinePlaceholder":515},[137,86950,86951],{"class":139,"line":24199},[137,86952,86953],{},"1. Identify changed files in `content\u002F` from the most recent push\n",[137,86955,86956],{"class":139,"line":24773},[137,86957,86958],{},"2. Validate frontmatter completeness and types\n",[137,86960,86961],{"class":139,"line":24778},[137,86962,86963],{},"3. Check SEO quality (description length, keyword count, title length)\n",[137,86965,86966],{"class":139,"line":24783},[137,86967,86968],{},"4. Check tag consistency (exactly 3 from established list)\n",[137,86970,86971],{"class":139,"line":24793},[137,86972,86973],{},"5. Check external links include `target=\"_blank\"` and `rel=\"noopener noreferrer\"`\n",[137,86975,86976],{"class":139,"line":24827},[137,86977,86978],{},"6. Flag very short articles (under 500 words) or very long ones (over 5000 words)\n",[137,86980,86981],{"class":139,"line":24857},[137,86982,516],{"emptyLinePlaceholder":515},[137,86984,86985],{"class":139,"line":24862},[137,86986,86987],{},"## Output Format\n",[137,86989,86990],{"class":139,"line":24867},[137,86991,516],{"emptyLinePlaceholder":515},[137,86993,86994],{"class":139,"line":24884},[137,86995,86996],{},"Create a single issue with findings for each article, including\n",[137,86998,86999],{"class":139,"line":24892},[137,87000,87001],{},"a quality score: Excellent \u002F Good \u002F Needs Attention.\n",[123,87003,87005],{"id":87004},"workflow-2-weekly-content-health-report","Workflow 2: Weekly Content Health Report",[27,87007,87008,87010],{},[42,87009,86737],{}," With over 44 articles spanning 5 years, it's hard to keep track of publishing trends, which topics are underrepresented, or which articles are missing audio summaries.",[27,87012,87013,87015],{},[42,87014,86743],{}," A weekly report generated as a GitHub issue.",[128,87017,87019],{"className":70935,"code":87018,"language":57972,"meta":133,"style":133},"---\non:\n    schedule: weekly\n    workflow_dispatch:\n\npermissions:\n    contents: read\n    issues: read\n    pull-requests: read\n\nsafe-outputs:\n    create-issue:\n        title-prefix: \"[Weekly Report] \"\n        labels: [report]\n        group: true\n        expires: 14\n\ntools:\n    github:\n---\n\n# Weekly Content Health Report\n\nGenerate a comprehensive weekly health report for the blog.\n\n## What to Analyze\n\n1. **Publishing Cadence** - Articles per year, current month vs previous,\n   longest gap between publications\n2. **Tag Distribution** - Most\u002Fleast covered topics, underrepresented areas\n3. **Audio Summary Coverage** - Cross-reference articles in `content\u002F`\n   with audio summaries in `audio-summary\u002F`\n4. **SEO Health Check** - Short descriptions, missing keywords,\n   overly long titles\n5. **Recent Activity** - Latest commits, open issues, merged PRs\n\n## Report Format\n\nStructure as a well-formatted issue with an Executive Summary,\neach section as a separate heading, tables where appropriate,\nand 3-5 actionable Recommendations at the end.\n",[22,87020,87021,87025,87029,87034,87039,87043,87047,87051,87055,87059,87063,87067,87071,87076,87081,87086,87091,87095,87099,87103,87107,87111,87116,87120,87125,87129,87134,87138,87143,87148,87153,87158,87163,87168,87173,87178,87182,87187,87191,87196,87201],{"__ignoreMap":133},[137,87022,87023],{"class":139,"line":140},[137,87024,70943],{},[137,87026,87027],{"class":139,"line":173},[137,87028,85113],{},[137,87030,87031],{"class":139,"line":188},[137,87032,87033],{},"    schedule: weekly\n",[137,87035,87036],{"class":139,"line":269},[137,87037,87038],{},"    workflow_dispatch:\n",[137,87040,87041],{"class":139,"line":278},[137,87042,516],{"emptyLinePlaceholder":515},[137,87044,87045],{"class":139,"line":291},[137,87046,85132],{},[137,87048,87049],{"class":139,"line":297},[137,87050,85137],{},[137,87052,87053],{"class":139,"line":302},[137,87054,85142],{},[137,87056,87057],{"class":139,"line":662},[137,87058,85147],{},[137,87060,87061],{"class":139,"line":667},[137,87062,516],{"emptyLinePlaceholder":515},[137,87064,87065],{"class":139,"line":786},[137,87066,85156],{},[137,87068,87069],{"class":139,"line":798},[137,87070,86814],{},[137,87072,87073],{"class":139,"line":803},[137,87074,87075],{},"        title-prefix: \"[Weekly Report] \"\n",[137,87077,87078],{"class":139,"line":931},[137,87079,87080],{},"        labels: [report]\n",[137,87082,87083],{"class":139,"line":1568},[137,87084,87085],{},"        group: true\n",[137,87087,87088],{"class":139,"line":1573},[137,87089,87090],{},"        expires: 14\n",[137,87092,87093],{"class":139,"line":1578},[137,87094,516],{"emptyLinePlaceholder":515},[137,87096,87097],{"class":139,"line":1588},[137,87098,85180],{},[137,87100,87101],{"class":139,"line":1601},[137,87102,85185],{},[137,87104,87105],{"class":139,"line":3802},[137,87106,70943],{},[137,87108,87109],{"class":139,"line":3808},[137,87110,516],{"emptyLinePlaceholder":515},[137,87112,87113],{"class":139,"line":3822},[137,87114,87115],{},"# Weekly Content Health Report\n",[137,87117,87118],{"class":139,"line":3827},[137,87119,516],{"emptyLinePlaceholder":515},[137,87121,87122],{"class":139,"line":3832},[137,87123,87124],{},"Generate a comprehensive weekly health report for the blog.\n",[137,87126,87127],{"class":139,"line":3840},[137,87128,516],{"emptyLinePlaceholder":515},[137,87130,87131],{"class":139,"line":3846},[137,87132,87133],{},"## What to Analyze\n",[137,87135,87136],{"class":139,"line":3861},[137,87137,516],{"emptyLinePlaceholder":515},[137,87139,87140],{"class":139,"line":3883},[137,87141,87142],{},"1. **Publishing Cadence** - Articles per year, current month vs previous,\n",[137,87144,87145],{"class":139,"line":3896},[137,87146,87147],{},"   longest gap between publications\n",[137,87149,87150],{"class":139,"line":3901},[137,87151,87152],{},"2. **Tag Distribution** - Most\u002Fleast covered topics, underrepresented areas\n",[137,87154,87155],{"class":139,"line":3906},[137,87156,87157],{},"3. **Audio Summary Coverage** - Cross-reference articles in `content\u002F`\n",[137,87159,87160],{"class":139,"line":3911},[137,87161,87162],{},"   with audio summaries in `audio-summary\u002F`\n",[137,87164,87165],{"class":139,"line":4666},[137,87166,87167],{},"4. **SEO Health Check** - Short descriptions, missing keywords,\n",[137,87169,87170],{"class":139,"line":4672},[137,87171,87172],{},"   overly long titles\n",[137,87174,87175],{"class":139,"line":4680},[137,87176,87177],{},"5. **Recent Activity** - Latest commits, open issues, merged PRs\n",[137,87179,87180],{"class":139,"line":4711},[137,87181,516],{"emptyLinePlaceholder":515},[137,87183,87184],{"class":139,"line":4716},[137,87185,87186],{},"## Report Format\n",[137,87188,87189],{"class":139,"line":4721},[137,87190,516],{"emptyLinePlaceholder":515},[137,87192,87193],{"class":139,"line":4727},[137,87194,87195],{},"Structure as a well-formatted issue with an Executive Summary,\n",[137,87197,87198],{"class":139,"line":4732},[137,87199,87200],{},"each section as a separate heading, tables where appropriate,\n",[137,87202,87203],{"class":139,"line":5006},[137,87204,87205],{},"and 3-5 actionable Recommendations at the end.\n",[123,87207,87209],{"id":87208},"workflow-3-issue-triage","Workflow 3: Issue Triage",[27,87211,87212,87214],{},[42,87213,86737],{}," When someone opens an issue on my blog's repository - a bug report, a content request, a typo fix - I want them to get a quick, helpful response even when I'm not available.",[27,87216,87217,87219],{},[42,87218,86743],{}," Automatic triage with labels and a first-response comment.",[128,87221,87223],{"className":70935,"code":87222,"language":57972,"meta":133,"style":133},"---\non:\n    issues:\n        types: [opened, reopened]\n\npermissions:\n    contents: read\n    issues: read\n    pull-requests: read\n\nsafe-outputs:\n    add-comment: {}\n    add-labels:\n        allowed: [bug, content-request, enhancement, typo, question]\n\ntools:\n    github:\n---\n\n# Blog Issue Triage\n\nWhen a new issue is opened or reopened, analyze it and\nprovide an initial response with appropriate categorization.\n\n## What to Do\n\n1. Read the issue carefully\n2. Research the codebase for context\n3. Apply exactly ONE label from the allowed list\n4. Leave a helpful comment that includes:\n    - Acknowledgment of the issue\n    - Your analysis (what you found in the codebase)\n    - A friendly note that Aleksandar will review it soon\n\n## Tone\n\nBe friendly, helpful, and professional. Thank the person\nfor opening the issue.\n",[22,87224,87225,87229,87233,87237,87241,87245,87249,87253,87257,87261,87265,87269,87273,87277,87282,87286,87290,87294,87298,87302,87307,87311,87316,87321,87325,87330,87334,87339,87343,87348,87353,87358,87363,87368,87372,87376,87380,87385],{"__ignoreMap":133},[137,87226,87227],{"class":139,"line":140},[137,87228,70943],{},[137,87230,87231],{"class":139,"line":173},[137,87232,85113],{},[137,87234,87235],{"class":139,"line":188},[137,87236,85118],{},[137,87238,87239],{"class":139,"line":269},[137,87240,85123],{},[137,87242,87243],{"class":139,"line":278},[137,87244,516],{"emptyLinePlaceholder":515},[137,87246,87247],{"class":139,"line":291},[137,87248,85132],{},[137,87250,87251],{"class":139,"line":297},[137,87252,85137],{},[137,87254,87255],{"class":139,"line":302},[137,87256,85142],{},[137,87258,87259],{"class":139,"line":662},[137,87260,85147],{},[137,87262,87263],{"class":139,"line":667},[137,87264,516],{"emptyLinePlaceholder":515},[137,87266,87267],{"class":139,"line":786},[137,87268,85156],{},[137,87270,87271],{"class":139,"line":798},[137,87272,85161],{},[137,87274,87275],{"class":139,"line":803},[137,87276,85166],{},[137,87278,87279],{"class":139,"line":931},[137,87280,87281],{},"        allowed: [bug, content-request, enhancement, typo, question]\n",[137,87283,87284],{"class":139,"line":1568},[137,87285,516],{"emptyLinePlaceholder":515},[137,87287,87288],{"class":139,"line":1573},[137,87289,85180],{},[137,87291,87292],{"class":139,"line":1578},[137,87293,85185],{},[137,87295,87296],{"class":139,"line":1588},[137,87297,70943],{},[137,87299,87300],{"class":139,"line":1601},[137,87301,516],{"emptyLinePlaceholder":515},[137,87303,87304],{"class":139,"line":3802},[137,87305,87306],{},"# Blog Issue Triage\n",[137,87308,87309],{"class":139,"line":3808},[137,87310,516],{"emptyLinePlaceholder":515},[137,87312,87313],{"class":139,"line":3822},[137,87314,87315],{},"When a new issue is opened or reopened, analyze it and\n",[137,87317,87318],{"class":139,"line":3827},[137,87319,87320],{},"provide an initial response with appropriate categorization.\n",[137,87322,87323],{"class":139,"line":3832},[137,87324,516],{"emptyLinePlaceholder":515},[137,87326,87327],{"class":139,"line":3840},[137,87328,87329],{},"## What to Do\n",[137,87331,87332],{"class":139,"line":3846},[137,87333,516],{"emptyLinePlaceholder":515},[137,87335,87336],{"class":139,"line":3861},[137,87337,87338],{},"1. Read the issue carefully\n",[137,87340,87341],{"class":139,"line":3883},[137,87342,85230],{},[137,87344,87345],{"class":139,"line":3896},[137,87346,87347],{},"3. Apply exactly ONE label from the allowed list\n",[137,87349,87350],{"class":139,"line":3901},[137,87351,87352],{},"4. Leave a helpful comment that includes:\n",[137,87354,87355],{"class":139,"line":3906},[137,87356,87357],{},"    - Acknowledgment of the issue\n",[137,87359,87360],{"class":139,"line":3911},[137,87361,87362],{},"    - Your analysis (what you found in the codebase)\n",[137,87364,87365],{"class":139,"line":4666},[137,87366,87367],{},"    - A friendly note that Aleksandar will review it soon\n",[137,87369,87370],{"class":139,"line":4672},[137,87371,516],{"emptyLinePlaceholder":515},[137,87373,87374],{"class":139,"line":4680},[137,87375,86407],{},[137,87377,87378],{"class":139,"line":4711},[137,87379,516],{"emptyLinePlaceholder":515},[137,87381,87382],{"class":139,"line":4716},[137,87383,87384],{},"Be friendly, helpful, and professional. Thank the person\n",[137,87386,87387],{"class":139,"line":4721},[137,87388,87389],{},"for opening the issue.\n",[123,87391,87393],{"id":87392},"workflow-4-pr-content-reviewer","Workflow 4: PR Content Reviewer",[27,87395,87396,87398],{},[42,87397,86737],{}," When a pull request modifies blog articles, I want automated feedback on frontmatter validity, formatting consistency, and link safety.",[128,87400,87402],{"className":70935,"code":87401,"language":57972,"meta":133,"style":133},"---\non:\n    pull_request:\n        types: [opened, synchronize]\n\npermissions:\n    contents: read\n    issues: read\n    pull-requests: read\n\nsafe-outputs:\n    add-comment: {}\n\ntools:\n    github:\n---\n\n# Pull Request Content Reviewer\n\nWhen a pull request is opened or updated, review the changes\nfor quality and consistency.\n\n## Review Checklist for Content Changes\n\n1. All required frontmatter fields present with correct types\n2. `articleTags` has exactly 3 entries\n3. `description` is 120-160 characters\n4. Code blocks specify a language for syntax highlighting\n5. External links include `target=\"_blank\"` and `rel=\"noopener noreferrer\"`\n\n## Review Checklist for Code Changes\n\n1. TypeScript type safety and Vue 3 Composition API patterns\n2. Consistent use of Tailwind CSS utility classes\n3. Proper ARIA attributes and semantic HTML\n\n## Comment Format\n\nPost a single review comment with categorized findings\nand an overall assessment: Looks Good \u002F Minor Issues \u002F Needs Attention.\n",[22,87403,87404,87408,87412,87417,87422,87426,87430,87434,87438,87442,87446,87450,87454,87458,87462,87466,87470,87474,87479,87483,87488,87493,87497,87502,87506,87511,87516,87521,87526,87531,87535,87540,87544,87549,87554,87559,87563,87568,87572,87577],{"__ignoreMap":133},[137,87405,87406],{"class":139,"line":140},[137,87407,70943],{},[137,87409,87410],{"class":139,"line":173},[137,87411,85113],{},[137,87413,87414],{"class":139,"line":188},[137,87415,87416],{},"    pull_request:\n",[137,87418,87419],{"class":139,"line":269},[137,87420,87421],{},"        types: [opened, synchronize]\n",[137,87423,87424],{"class":139,"line":278},[137,87425,516],{"emptyLinePlaceholder":515},[137,87427,87428],{"class":139,"line":291},[137,87429,85132],{},[137,87431,87432],{"class":139,"line":297},[137,87433,85137],{},[137,87435,87436],{"class":139,"line":302},[137,87437,85142],{},[137,87439,87440],{"class":139,"line":662},[137,87441,85147],{},[137,87443,87444],{"class":139,"line":667},[137,87445,516],{"emptyLinePlaceholder":515},[137,87447,87448],{"class":139,"line":786},[137,87449,85156],{},[137,87451,87452],{"class":139,"line":798},[137,87453,85161],{},[137,87455,87456],{"class":139,"line":803},[137,87457,516],{"emptyLinePlaceholder":515},[137,87459,87460],{"class":139,"line":931},[137,87461,85180],{},[137,87463,87464],{"class":139,"line":1568},[137,87465,85185],{},[137,87467,87468],{"class":139,"line":1573},[137,87469,70943],{},[137,87471,87472],{"class":139,"line":1578},[137,87473,516],{"emptyLinePlaceholder":515},[137,87475,87476],{"class":139,"line":1588},[137,87477,87478],{},"# Pull Request Content Reviewer\n",[137,87480,87481],{"class":139,"line":1601},[137,87482,516],{"emptyLinePlaceholder":515},[137,87484,87485],{"class":139,"line":3802},[137,87486,87487],{},"When a pull request is opened or updated, review the changes\n",[137,87489,87490],{"class":139,"line":3808},[137,87491,87492],{},"for quality and consistency.\n",[137,87494,87495],{"class":139,"line":3822},[137,87496,516],{"emptyLinePlaceholder":515},[137,87498,87499],{"class":139,"line":3827},[137,87500,87501],{},"## Review Checklist for Content Changes\n",[137,87503,87504],{"class":139,"line":3832},[137,87505,516],{"emptyLinePlaceholder":515},[137,87507,87508],{"class":139,"line":3840},[137,87509,87510],{},"1. All required frontmatter fields present with correct types\n",[137,87512,87513],{"class":139,"line":3846},[137,87514,87515],{},"2. `articleTags` has exactly 3 entries\n",[137,87517,87518],{"class":139,"line":3861},[137,87519,87520],{},"3. `description` is 120-160 characters\n",[137,87522,87523],{"class":139,"line":3883},[137,87524,87525],{},"4. Code blocks specify a language for syntax highlighting\n",[137,87527,87528],{"class":139,"line":3896},[137,87529,87530],{},"5. External links include `target=\"_blank\"` and `rel=\"noopener noreferrer\"`\n",[137,87532,87533],{"class":139,"line":3901},[137,87534,516],{"emptyLinePlaceholder":515},[137,87536,87537],{"class":139,"line":3906},[137,87538,87539],{},"## Review Checklist for Code Changes\n",[137,87541,87542],{"class":139,"line":3911},[137,87543,516],{"emptyLinePlaceholder":515},[137,87545,87546],{"class":139,"line":4666},[137,87547,87548],{},"1. TypeScript type safety and Vue 3 Composition API patterns\n",[137,87550,87551],{"class":139,"line":4672},[137,87552,87553],{},"2. Consistent use of Tailwind CSS utility classes\n",[137,87555,87556],{"class":139,"line":4680},[137,87557,87558],{},"3. Proper ARIA attributes and semantic HTML\n",[137,87560,87561],{"class":139,"line":4711},[137,87562,516],{"emptyLinePlaceholder":515},[137,87564,87565],{"class":139,"line":4716},[137,87566,87567],{},"## Comment Format\n",[137,87569,87570],{"class":139,"line":4721},[137,87571,516],{"emptyLinePlaceholder":515},[137,87573,87574],{"class":139,"line":4727},[137,87575,87576],{},"Post a single review comment with categorized findings\n",[137,87578,87579],{"class":139,"line":4732},[137,87580,87581],{},"and an overall assessment: Looks Good \u002F Minor Issues \u002F Needs Attention.\n",[123,87583,87585],{"id":87584},"workflow-5-ci-doctor-chatops","Workflow 5: CI Doctor (ChatOps)",[27,87587,87588,87590],{},[42,87589,86737],{}," When a build fails in CI, debugging means opening the Actions tab, finding the failed run, expanding the logs, and reading through them. It's tedious.",[27,87592,87593,87595,87596,87599],{},[42,87594,86743],{}," Comment ",[22,87597,87598],{},"\u002Ffix"," on a PR and the agent investigates for you.",[128,87601,87603],{"className":70935,"code":87602,"language":57972,"meta":133,"style":133},"---\non:\n    issue_comment:\n        types: [created]\n\npermissions:\n    contents: read\n    actions: read\n    pull-requests: read\n    issues: read\n\nsafe-outputs:\n    add-comment: {}\n    create-pull-request:\n        title-prefix: \"[CI Fix] \"\n        labels: [ci-fix]\n\ntools:\n    github:\n---\n\n# CI Doctor\n\nWhen someone comments `\u002Ffix` on a pull request or issue,\ninvestigate failing CI checks and propose fixes.\n\n## What to Do\n\n1. Only act when the first line of the comment is `\u002Ffix`\n2. Find failing workflow runs associated with the PR\n3. Analyze failure logs to identify the specific error\n4. Research the codebase for context\n5. Post a comment with:\n    - **Diagnosis** - what failed and why\n    - **Root Cause** - the underlying issue\n    - **Suggested Fix** - specific code changes needed\n    - **Prevention** - how to avoid this in the future\n",[22,87604,87605,87609,87613,87618,87623,87627,87631,87635,87640,87644,87648,87652,87656,87660,87665,87670,87675,87679,87683,87687,87691,87695,87700,87704,87709,87714,87718,87722,87726,87731,87736,87741,87746,87751,87756,87761,87766],{"__ignoreMap":133},[137,87606,87607],{"class":139,"line":140},[137,87608,70943],{},[137,87610,87611],{"class":139,"line":173},[137,87612,85113],{},[137,87614,87615],{"class":139,"line":188},[137,87616,87617],{},"    issue_comment:\n",[137,87619,87620],{"class":139,"line":269},[137,87621,87622],{},"        types: [created]\n",[137,87624,87625],{"class":139,"line":278},[137,87626,516],{"emptyLinePlaceholder":515},[137,87628,87629],{"class":139,"line":291},[137,87630,85132],{},[137,87632,87633],{"class":139,"line":297},[137,87634,85137],{},[137,87636,87637],{"class":139,"line":302},[137,87638,87639],{},"    actions: read\n",[137,87641,87642],{"class":139,"line":662},[137,87643,85147],{},[137,87645,87646],{"class":139,"line":667},[137,87647,85142],{},[137,87649,87650],{"class":139,"line":786},[137,87651,516],{"emptyLinePlaceholder":515},[137,87653,87654],{"class":139,"line":798},[137,87655,85156],{},[137,87657,87658],{"class":139,"line":803},[137,87659,85161],{},[137,87661,87662],{"class":139,"line":931},[137,87663,87664],{},"    create-pull-request:\n",[137,87666,87667],{"class":139,"line":1568},[137,87668,87669],{},"        title-prefix: \"[CI Fix] \"\n",[137,87671,87672],{"class":139,"line":1573},[137,87673,87674],{},"        labels: [ci-fix]\n",[137,87676,87677],{"class":139,"line":1578},[137,87678,516],{"emptyLinePlaceholder":515},[137,87680,87681],{"class":139,"line":1588},[137,87682,85180],{},[137,87684,87685],{"class":139,"line":1601},[137,87686,85185],{},[137,87688,87689],{"class":139,"line":3802},[137,87690,70943],{},[137,87692,87693],{"class":139,"line":3808},[137,87694,516],{"emptyLinePlaceholder":515},[137,87696,87697],{"class":139,"line":3822},[137,87698,87699],{},"# CI Doctor\n",[137,87701,87702],{"class":139,"line":3827},[137,87703,516],{"emptyLinePlaceholder":515},[137,87705,87706],{"class":139,"line":3832},[137,87707,87708],{},"When someone comments `\u002Ffix` on a pull request or issue,\n",[137,87710,87711],{"class":139,"line":3840},[137,87712,87713],{},"investigate failing CI checks and propose fixes.\n",[137,87715,87716],{"class":139,"line":3846},[137,87717,516],{"emptyLinePlaceholder":515},[137,87719,87720],{"class":139,"line":3861},[137,87721,87329],{},[137,87723,87724],{"class":139,"line":3883},[137,87725,516],{"emptyLinePlaceholder":515},[137,87727,87728],{"class":139,"line":3896},[137,87729,87730],{},"1. Only act when the first line of the comment is `\u002Ffix`\n",[137,87732,87733],{"class":139,"line":3901},[137,87734,87735],{},"2. Find failing workflow runs associated with the PR\n",[137,87737,87738],{"class":139,"line":3906},[137,87739,87740],{},"3. Analyze failure logs to identify the specific error\n",[137,87742,87743],{"class":139,"line":3911},[137,87744,87745],{},"4. Research the codebase for context\n",[137,87747,87748],{"class":139,"line":4666},[137,87749,87750],{},"5. Post a comment with:\n",[137,87752,87753],{"class":139,"line":4672},[137,87754,87755],{},"    - **Diagnosis** - what failed and why\n",[137,87757,87758],{"class":139,"line":4680},[137,87759,87760],{},"    - **Root Cause** - the underlying issue\n",[137,87762,87763],{"class":139,"line":4711},[137,87764,87765],{},"    - **Suggested Fix** - specific code changes needed\n",[137,87767,87768],{"class":139,"line":4716},[137,87769,87770],{},"    - **Prevention** - how to avoid this in the future\n",[123,87772,87774],{"id":87773},"workflow-6-stale-content-detector","Workflow 6: Stale Content Detector",[27,87776,87777,87779],{},[42,87778,86737],{}," Technical blog content goes stale quickly. Articles about Vue 2, Node 16, or old API patterns might mislead readers if left without a disclaimer.",[27,87781,87782,87784],{},[42,87783,86743],{}," A monthly audit that identifies potentially outdated content.",[128,87786,87788],{"className":70935,"code":87787,"language":57972,"meta":133,"style":133},"---\non:\n    schedule:\n        - cron: \"0 0 1 * *\"\n    workflow_dispatch:\n\npermissions:\n    contents: read\n    issues: read\n    pull-requests: read\n\nsafe-outputs:\n    create-issue:\n        title-prefix: \"[Content Freshness] \"\n        labels: [maintenance]\n        group: true\n        expires: 30\n\ntools:\n    github:\n---\n\n# Stale Content Detector\n\nPerform a monthly audit of all blog articles to identify\ncontent that may be outdated.\n\n## What to Analyze\n\n1. **Age-Based Staleness** - Articles about fast-moving technologies\n   (Vue.js, Node.js, AI) older than 18 months are high risk\n2. **Version References** - Scan for specific version numbers,\n   deprecated APIs, pinned package versions\n3. **Technology Lifecycle** - Flag Vue 2 content, end-of-life\n   Node.js versions, rapidly evolving AI libraries\n\n## Report Format\n\nCreate a single issue organized by priority:\nHigh (misleading if left as-is), Medium (would benefit from refresh),\nand Low (keep on radar). End with summary statistics.\n",[22,87789,87790,87794,87798,87803,87808,87812,87816,87820,87824,87828,87832,87836,87840,87844,87849,87854,87858,87863,87867,87871,87875,87879,87883,87888,87892,87897,87902,87906,87910,87914,87919,87924,87929,87934,87939,87944,87948,87952,87956,87961,87966],{"__ignoreMap":133},[137,87791,87792],{"class":139,"line":140},[137,87793,70943],{},[137,87795,87796],{"class":139,"line":173},[137,87797,85113],{},[137,87799,87800],{"class":139,"line":188},[137,87801,87802],{},"    schedule:\n",[137,87804,87805],{"class":139,"line":269},[137,87806,87807],{},"        - cron: \"0 0 1 * *\"\n",[137,87809,87810],{"class":139,"line":278},[137,87811,87038],{},[137,87813,87814],{"class":139,"line":291},[137,87815,516],{"emptyLinePlaceholder":515},[137,87817,87818],{"class":139,"line":297},[137,87819,85132],{},[137,87821,87822],{"class":139,"line":302},[137,87823,85137],{},[137,87825,87826],{"class":139,"line":662},[137,87827,85142],{},[137,87829,87830],{"class":139,"line":667},[137,87831,85147],{},[137,87833,87834],{"class":139,"line":786},[137,87835,516],{"emptyLinePlaceholder":515},[137,87837,87838],{"class":139,"line":798},[137,87839,85156],{},[137,87841,87842],{"class":139,"line":803},[137,87843,86814],{},[137,87845,87846],{"class":139,"line":931},[137,87847,87848],{},"        title-prefix: \"[Content Freshness] \"\n",[137,87850,87851],{"class":139,"line":1568},[137,87852,87853],{},"        labels: [maintenance]\n",[137,87855,87856],{"class":139,"line":1573},[137,87857,87085],{},[137,87859,87860],{"class":139,"line":1578},[137,87861,87862],{},"        expires: 30\n",[137,87864,87865],{"class":139,"line":1588},[137,87866,516],{"emptyLinePlaceholder":515},[137,87868,87869],{"class":139,"line":1601},[137,87870,85180],{},[137,87872,87873],{"class":139,"line":3802},[137,87874,85185],{},[137,87876,87877],{"class":139,"line":3808},[137,87878,70943],{},[137,87880,87881],{"class":139,"line":3822},[137,87882,516],{"emptyLinePlaceholder":515},[137,87884,87885],{"class":139,"line":3827},[137,87886,87887],{},"# Stale Content Detector\n",[137,87889,87890],{"class":139,"line":3832},[137,87891,516],{"emptyLinePlaceholder":515},[137,87893,87894],{"class":139,"line":3840},[137,87895,87896],{},"Perform a monthly audit of all blog articles to identify\n",[137,87898,87899],{"class":139,"line":3846},[137,87900,87901],{},"content that may be outdated.\n",[137,87903,87904],{"class":139,"line":3861},[137,87905,516],{"emptyLinePlaceholder":515},[137,87907,87908],{"class":139,"line":3883},[137,87909,87133],{},[137,87911,87912],{"class":139,"line":3896},[137,87913,516],{"emptyLinePlaceholder":515},[137,87915,87916],{"class":139,"line":3901},[137,87917,87918],{},"1. **Age-Based Staleness** - Articles about fast-moving technologies\n",[137,87920,87921],{"class":139,"line":3906},[137,87922,87923],{},"   (Vue.js, Node.js, AI) older than 18 months are high risk\n",[137,87925,87926],{"class":139,"line":3911},[137,87927,87928],{},"2. **Version References** - Scan for specific version numbers,\n",[137,87930,87931],{"class":139,"line":4666},[137,87932,87933],{},"   deprecated APIs, pinned package versions\n",[137,87935,87936],{"class":139,"line":4672},[137,87937,87938],{},"3. **Technology Lifecycle** - Flag Vue 2 content, end-of-life\n",[137,87940,87941],{"class":139,"line":4680},[137,87942,87943],{},"   Node.js versions, rapidly evolving AI libraries\n",[137,87945,87946],{"class":139,"line":4711},[137,87947,516],{"emptyLinePlaceholder":515},[137,87949,87950],{"class":139,"line":4716},[137,87951,87186],{},[137,87953,87954],{"class":139,"line":4721},[137,87955,516],{"emptyLinePlaceholder":515},[137,87957,87958],{"class":139,"line":4727},[137,87959,87960],{},"Create a single issue organized by priority:\n",[137,87962,87963],{"class":139,"line":4732},[137,87964,87965],{},"High (misleading if left as-is), Medium (would benefit from refresh),\n",[137,87967,87968],{"class":139,"line":5006},[137,87969,87970],{},"and Low (keep on radar). End with summary statistics.\n",[104,87972,82351],{"id":82350},[27,87974,87975],{},"GitHub Agentic Workflows is still in technical preview. Things will change - the CLI commands might evolve, new tools will be added, and the security model will mature. But even in this early state, the concept is compelling: describe what you want in plain language, set clear boundaries, and let an AI agent handle the execution.",[27,87977,87978],{},"What I find most interesting is the security architecture. The read-only-by-default model with safe outputs as controlled write channels is a thoughtful approach. The agent can analyse your entire repository, reason about what needs to happen, and propose changes - but it can never directly modify anything without going through the approved channels.",[27,87980,87981],{},"For my blog, these six workflows handle tasks that I used to do manually (or more often, forgot to do entirely). The article quality reviewer catches mistakes before they go live. The weekly report gives me a bird's-eye view I'd never take the time to generate myself. The issue triage provides immediate responses to anyone who opens an issue, even when I'm not at my desk.",[27,87983,87984,87985,87990,87991,87996,87997,88000,88001,1017],{},"If you want to explore further, the official documentation is at ",[45,87986,87989],{"href":87987,"target":2716,"rel":87988},"https:\u002F\u002Fgithub.github.io\u002Fgh-aw\u002F",[2718,2719],"github.github.io\u002Fgh-aw",", and there's a sample pack of ready-to-use workflows at ",[45,87992,87995],{"href":87993,"target":2716,"rel":87994},"https:\u002F\u002Fgithub.com\u002Fgithubnext\u002Fagentics",[2718,2719],"github.com\u002Fgithubnext\u002Fagentics",". The six workflows I built for this blog are in the ",[45,87998,77779],{"href":47632,"target":2716,"rel":87999},[2718,2719]," under ",[22,88002,76051],{},[27,88004,88005],{},"Happy automating!",[2617,88007,88008],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":133,"searchDepth":173,"depth":173,"links":88010},[88011,88012,88013,88017,88022,88037,88050,88058],{"id":84523,"depth":173,"text":84524},{"id":84595,"depth":173,"text":84596},{"id":66828,"depth":173,"text":66831,"children":88014},[88015,88016],{"id":84668,"depth":188,"text":84669},{"id":84710,"depth":188,"text":84711},{"id":84758,"depth":173,"text":84759,"children":88018},[88019,88020,88021],{"id":84776,"depth":188,"text":84777},{"id":84874,"depth":188,"text":84875},{"id":84948,"depth":188,"text":84949},{"id":85095,"depth":173,"text":85096,"children":88023},[88024,88026,88028,88030,88032,88034,88036],{"id":85246,"depth":188,"text":88025},"The on: Property - Triggers",{"id":85518,"depth":188,"text":88027},"The permissions: Property",{"id":85643,"depth":188,"text":88029},"The engine: Property",{"id":85751,"depth":188,"text":88031},"The tools: Property",{"id":86063,"depth":188,"text":88033},"The safe-outputs: Property",{"id":86316,"depth":188,"text":88035},"The network: Property",{"id":86349,"depth":188,"text":86350},{"id":86422,"depth":173,"text":86423,"children":88038},[88039,88041,88043,88045,88047,88049],{"id":86426,"depth":188,"text":88040},"What Is gh aw compile?",{"id":86503,"depth":188,"text":88042},"The .lock.yml Files",{"id":86519,"depth":188,"text":88044},"The actions-lock.json File",{"id":86626,"depth":188,"text":88046},"The agentics-maintenance.yml File",{"id":86655,"depth":188,"text":88048},"The .gitattributes File",{"id":86679,"depth":188,"text":86680},{"id":86724,"depth":173,"text":86725,"children":88051},[88052,88053,88054,88055,88056,88057],{"id":86731,"depth":188,"text":86732},{"id":87004,"depth":188,"text":87005},{"id":87208,"depth":188,"text":87209},{"id":87392,"depth":188,"text":87393},{"id":87584,"depth":188,"text":87585},{"id":87773,"depth":188,"text":87774},{"id":82350,"depth":173,"text":82351},"Discover how to use GitHub Agentic Workflows to automate repository tasks with AI agents. This step-by-step tutorial covers writing workflows in plain Markdown, configuring triggers, permissions, tools, and safe outputs, and building practical automations for your projects.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1771503278\u002Fblog\u002Fgithub-agentic-workflows-build-github-actions-in-markdown-with-ai-agents\u002Fgithub-agentic-workflows-build-github-actions-in-markdown-with-ai-agents_r153nu",[84512,80385,88062,88063,88064,88065,80391,84765,88066,88067,84649,85762,72676,88068,80582,88069,88070,88071,82411,88072],"AI automation","repository automation","gh-aw CLI","agentic AI","Claude engine","Codex engine","CI\u002FCD automation","Markdown workflows","issue triage","pull request review","GitHub CLI",{},"\u002F2026\u002F02\u002F22\u002Fagentic-workflows-write-github-actions-in-markdown","22nd February 2026",{"title":84468,"description":88059},"2026\u002F02\u002F22\u002Fagentic-workflows-write-github-actions-in-markdown","sllSNwW61q4U9FCkoiwXJEvREh0xKPd6PiAHoizoTns",{"id":88080,"title":88081,"articleTags":88082,"author":11,"blog":12,"body":88083,"description":89829,"extension":2649,"image":89830,"keywords":89831,"meta":89841,"navigation":515,"path":89842,"published":89843,"readTime":667,"seo":89844,"stem":89845,"type":2662,"__hash__":89846},"content\u002F2026\u002F03\u002F08\u002Fmcp-is-coming-to-the-browser.md","MCP Is Coming to the Browser: WebMCP and the Future of AI-Powered Websites",[27886,9,10],{"type":14,"value":88084,"toc":89800},[88085,88088,88103,88105,88109,88114,88117,88136,88139,88141,88145,88160,88163,88181,88194,88198,88209,88212,88243,88254,88258,88265,88268,88537,88540,88576,88583,88631,88635,88644,88670,88674,88677,88925,88928,88954,88972,88976,88979,89078,89081,89085,89094,89122,89129,89136,89140,89147,89170,89178,89182,89191,89205,89208,89313,89316,89320,89323,89331,89350,89368,89371,89376,89391,89396,89420,89429,89492,89503,89506,89509,89531,89537,89597,89604,89615,89622,89636,89645,89657,89661,89672,89680,89684,89687,89698,89700,89703,89705,89712,89714,89717,89719,89725,89727,89730,89732,89742,89746,89771,89778,89780,89783,89794,89797],[17,88086,88081],{"id":88087},"mcp-is-coming-to-the-browser-webmcp-and-the-future-of-ai-powered-websites",[27,88089,88090],{},[30,88091,88092,36,88094,88096,88097],{},[33,88093],{"value":35},[33,88095],{"value":39}," min read - by ",[42,88098,88099],{},[45,88100,88101],{"href":47},[33,88102],{"value":50},[52,88104],{":tags":54},[56,88106],{":audio-src":88107,":transcript-src":88108},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F03\u002F08\u002Fmcp-is-coming-to-the-browser\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F03\u002F08\u002Fmcp-is-coming-to-the-browser\u002Fsummary.json",[27,88110,88111],{},[63,88112],{"alt":12847,"src":88113},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1772448782\u002Fblog\u002Fmcp-is-coming-to-the-browser-webmcp-and-the-future-of-ai-powered-websites\u002Fhero-mcp-is-coming-to-the-browser_ntmegq",[27,88115,88116],{},"What if your website could talk directly to AI agents - not through brittle screen-scraping or DOM crawling, but through clean, structured tool calls? What if an agent could search your product catalogue, submit a form, or navigate your site the same way a developer would call an API?",[27,88118,88119,88126,88127,88132,88133,88135],{},[45,88120,88123],{"href":88121,"target":2716,"rel":88122},"https:\u002F\u002Fdeveloper.chrome.com\u002Fblog\u002Fwebmcp-epp",[2718,2719],[42,88124,88125],{},"WebMCP"," (Web Model Context Protocol) was released a few weeks ago as an early preview in Chrome, and it is a game changer. It's a new browser standard - developed by the ",[45,88128,88131],{"href":88129,"target":2716,"rel":88130},"https:\u002F\u002Fwebmachinelearning.github.io\u002Fwebmcp\u002F",[2718,2719],"W3C Web Machine Learning Community Group"," - that lets any website expose structured tools for AI agents to discover and use. No more fragile screen-scraping. No more navigating through nested ",[22,88134,3595],{},"s hoping to find the right button. Instead, your website tells the agent exactly what it can do, and the agent calls those tools with structured inputs and gets structured outputs back.",[27,88137,88138],{},"In this article, I'll explain what WebMCP is, walk you through both of its browser APIs (Imperative and Declarative), show you how to add it to any website regardless of framework, and demonstrate how to test your tools with real AI clients like VS Code and Claude Desktop. I'll also share how I added WebMCP to my own blog as a real-world example.",[27,88140,75994],{},[104,88142,88144],{"id":88143},"why-webmcp-is-a-game-changer","Why WebMCP Is a Game Changer",[27,88146,88147,88148,88151,88152,88155,88156,88159],{},"Imagine if every website on the web had WebMCP. You could ask any AI agent: ",[30,88149,88150],{},"\"Go to this e-commerce site and find me a blue jacket under $50\""," - and instead of the agent opening a browser, taking screenshots, parsing pixels, and clicking through filters one by one, the website would simply expose a ",[22,88153,88154],{},"search_products"," tool. The agent calls it with ",[22,88157,88158],{},"{ query: \"blue jacket\", maxPrice: 50 }"," and gets back structured JSON with exactly the results you need.",[27,88161,88162],{},"This will revolutionise how AI agents interact with the web. Instead of guessing and scraping, agents call well-defined, structured tools exposed directly by the website itself. It's the difference between:",[1003,88164,88165,88171],{},[1006,88166,88167,88170],{},[42,88168,88169],{},"Today",": Agent takes a screenshot → runs OCR (Optical Character Recognition) → guesses what to click → hopes it worked → takes another screenshot → repeats",[1006,88172,88173,88176,88177,88180],{},[42,88174,88175],{},"With WebMCP",": Agent calls ",[22,88178,88179],{},"search_products({ query: \"blue jacket\", maxPrice: 50 })"," → gets JSON back instantly",[27,88182,88183,88184,88189,88190,88193],{},"The token savings alone are significant. According to benchmarks from the ",[45,88185,88188],{"href":88186,"target":2716,"rel":88187},"https:\u002F\u002Fgithub.com\u002FWebMCP-org\u002Fchrome-devtools-quickstart",[2718,2719],"WebMCP quickstart project",", WebMCP tools use ",[42,88191,88192],{},"77–89% fewer tokens"," compared to screenshot-based approaches. That translates directly to lower costs and faster responses.",[104,88195,88197],{"id":88196},"what-is-webmcp","What Is WebMCP?",[27,88199,88200,88201,88204,88205,88208],{},"WebMCP extends the ",[45,88202,84643],{"href":84641,"target":2716,"rel":88203},[2718,2719]," - the open standard for connecting AI models to external tools and data sources - into the browser. It introduces a new browser API: ",[22,88206,88207],{},"navigator.modelContext",", which allows any web page to register tools that AI agents can discover and call.",[27,88210,88211],{},"The core idea is simple:",[2569,88213,88214,88221,88230,88237],{},[1006,88215,88216,88217,88220],{},"A website ",[42,88218,88219],{},"registers tools"," with names, descriptions, input schemas, and execute callbacks",[1006,88222,88223,88224,88227,88228],{},"An AI agent ",[42,88225,88226],{},"discovers"," those tools through ",[22,88229,88207],{},[1006,88231,88232,88233,88236],{},"The agent ",[42,88234,88235],{},"calls"," a tool with structured parameters",[1006,88238,88239,88240,88242],{},"The tool ",[42,88241,45728],{}," and returns structured results",[27,88244,88245,88246,88249,88250,88253],{},"WebMCP provides two ways to register tools: the ",[42,88247,88248],{},"Imperative API"," (JavaScript) and the ",[42,88251,88252],{},"Declarative API"," (HTML attributes). Let's look at both.",[104,88255,88257],{"id":88256},"the-imperative-api","The Imperative API",[27,88259,88260,88261,88264],{},"The Imperative API is the JavaScript approach. You call ",[22,88262,88263],{},"navigator.modelContext.registerTool()"," with a tool definition object. This gives you full control - you can run any logic in the execute callback, access application state, make API calls, or trigger navigation.",[27,88266,88267],{},"Here's a simple example - a tool that searches a product catalogue:",[128,88269,88271],{"className":130,"code":88270,"language":132,"meta":133,"style":133},"navigator.modelContext.registerTool({\n    name: \"search_products\",\n    description: \"Search for products by keyword, category, or price range\",\n    inputSchema: {\n        type: \"object\",\n        properties: {\n            query: {\n                type: \"string\",\n                description: \"Search keyword\",\n            },\n            category: {\n                type: \"string\",\n                description: \"Product category to filter by\",\n            },\n            maxPrice: {\n                type: \"number\",\n                description: \"Maximum price\",\n            },\n        },\n    },\n    execute: async ({ query, category, maxPrice }) => {\n        const results = await searchProducts({ query, category, maxPrice });\n\n        return {\n            content: [\n                {\n                    type: \"text\",\n                    text: JSON.stringify({\n                        count: results.length,\n                        products: results,\n                    }),\n                },\n            ],\n        };\n    },\n});\n",[22,88272,88273,88283,88292,88302,88307,88315,88320,88325,88334,88344,88348,88353,88361,88370,88374,88379,88388,88397,88401,88405,88409,88438,88455,88459,88465,88470,88475,88484,88497,88506,88511,88516,88520,88525,88529,88533],{"__ignoreMap":133},[137,88274,88275,88278,88281],{"class":139,"line":140},[137,88276,88277],{"class":157},"navigator.modelContext.",[137,88279,88280],{"class":147},"registerTool",[137,88282,3175],{"class":157},[137,88284,88285,88287,88290],{"class":139,"line":173},[137,88286,57427],{"class":157},[137,88288,88289],{"class":284},"\"search_products\"",[137,88291,1961],{"class":157},[137,88293,88294,88297,88300],{"class":139,"line":188},[137,88295,88296],{"class":157},"    description: ",[137,88298,88299],{"class":284},"\"Search for products by keyword, category, or price range\"",[137,88301,1961],{"class":157},[137,88303,88304],{"class":139,"line":269},[137,88305,88306],{"class":157},"    inputSchema: {\n",[137,88308,88309,88311,88313],{"class":139,"line":278},[137,88310,80945],{"class":157},[137,88312,83873],{"class":284},[137,88314,1961],{"class":157},[137,88316,88317],{"class":139,"line":291},[137,88318,88319],{"class":157},"        properties: {\n",[137,88321,88322],{"class":139,"line":297},[137,88323,88324],{"class":157},"            query: {\n",[137,88326,88327,88330,88332],{"class":139,"line":302},[137,88328,88329],{"class":157},"                type: ",[137,88331,67375],{"class":284},[137,88333,1961],{"class":157},[137,88335,88336,88339,88342],{"class":139,"line":662},[137,88337,88338],{"class":157},"                description: ",[137,88340,88341],{"class":284},"\"Search keyword\"",[137,88343,1961],{"class":157},[137,88345,88346],{"class":139,"line":667},[137,88347,14074],{"class":157},[137,88349,88350],{"class":139,"line":786},[137,88351,88352],{"class":157},"            category: {\n",[137,88354,88355,88357,88359],{"class":139,"line":798},[137,88356,88329],{"class":157},[137,88358,67375],{"class":284},[137,88360,1961],{"class":157},[137,88362,88363,88365,88368],{"class":139,"line":803},[137,88364,88338],{"class":157},[137,88366,88367],{"class":284},"\"Product category to filter by\"",[137,88369,1961],{"class":157},[137,88371,88372],{"class":139,"line":931},[137,88373,14074],{"class":157},[137,88375,88376],{"class":139,"line":1568},[137,88377,88378],{"class":157},"            maxPrice: {\n",[137,88380,88381,88383,88386],{"class":139,"line":1573},[137,88382,88329],{"class":157},[137,88384,88385],{"class":284},"\"number\"",[137,88387,1961],{"class":157},[137,88389,88390,88392,88395],{"class":139,"line":1578},[137,88391,88338],{"class":157},[137,88393,88394],{"class":284},"\"Maximum price\"",[137,88396,1961],{"class":157},[137,88398,88399],{"class":139,"line":1588},[137,88400,14074],{"class":157},[137,88402,88403],{"class":139,"line":1601},[137,88404,2084],{"class":157},[137,88406,88407],{"class":139,"line":3802},[137,88408,775],{"class":157},[137,88410,88411,88414,88416,88418,88420,88422,88424,88427,88429,88432,88434,88436],{"class":139,"line":3808},[137,88412,88413],{"class":147},"    execute",[137,88415,726],{"class":157},[137,88417,15050],{"class":143},[137,88419,75196],{"class":157},[137,88421,49767],{"class":161},[137,88423,164],{"class":157},[137,88425,88426],{"class":161},"category",[137,88428,164],{"class":157},[137,88430,88431],{"class":161},"maxPrice",[137,88433,29299],{"class":157},[137,88435,222],{"class":143},[137,88437,256],{"class":157},[137,88439,88440,88442,88445,88447,88449,88452],{"class":139,"line":3822},[137,88441,3008],{"class":143},[137,88443,88444],{"class":364}," results",[137,88446,151],{"class":143},[137,88448,15069],{"class":143},[137,88450,88451],{"class":147}," searchProducts",[137,88453,88454],{"class":157},"({ query, category, maxPrice });\n",[137,88456,88457],{"class":139,"line":3827},[137,88458,516],{"emptyLinePlaceholder":515},[137,88460,88461,88463],{"class":139,"line":3832},[137,88462,5472],{"class":143},[137,88464,256],{"class":157},[137,88466,88467],{"class":139,"line":3840},[137,88468,88469],{"class":157},"            content: [\n",[137,88471,88472],{"class":139,"line":3846},[137,88473,88474],{"class":157},"                {\n",[137,88476,88477,88480,88482],{"class":139,"line":3861},[137,88478,88479],{"class":157},"                    type: ",[137,88481,7837],{"class":284},[137,88483,1961],{"class":157},[137,88485,88486,88489,88491,88493,88495],{"class":139,"line":3883},[137,88487,88488],{"class":157},"                    text: ",[137,88490,22554],{"class":364},[137,88492,1017],{"class":157},[137,88494,24816],{"class":147},[137,88496,3175],{"class":157},[137,88498,88499,88502,88504],{"class":139,"line":3896},[137,88500,88501],{"class":157},"                        count: results.",[137,88503,8611],{"class":364},[137,88505,1961],{"class":157},[137,88507,88508],{"class":139,"line":3901},[137,88509,88510],{"class":157},"                        products: results,\n",[137,88512,88513],{"class":139,"line":3906},[137,88514,88515],{"class":157},"                    }),\n",[137,88517,88518],{"class":139,"line":3911},[137,88519,15680],{"class":157},[137,88521,88522],{"class":139,"line":4666},[137,88523,88524],{"class":157},"            ],\n",[137,88526,88527],{"class":139,"line":4672},[137,88528,1507],{"class":157},[137,88530,88531],{"class":139,"line":4680},[137,88532,775],{"class":157},[137,88534,88535],{"class":139,"line":4711},[137,88536,5422],{"class":157},[27,88538,88539],{},"Let me break down the key parts:",[1003,88541,88542,88549,88557,88565],{},[1006,88543,88544,88548],{},[42,88545,88546],{},[22,88547,1387],{}," - A unique identifier for the tool. Agents use this to call it.",[1006,88550,88551,88556],{},[42,88552,88553],{},[22,88554,88555],{},"description"," - A natural language description of what the tool does. This is what the agent reads to decide whether to use it.",[1006,88558,88559,88564],{},[42,88560,88561],{},[22,88562,88563],{},"inputSchema"," - A JSON Schema object describing the expected parameters. The agent uses this to construct valid inputs.",[1006,88566,88567,88572,88573,1017],{},[42,88568,88569],{},[22,88570,88571],{},"execute"," - An async callback that runs when the agent calls the tool. It receives the parameters and must return a result in the MCP content format: ",[22,88574,88575],{},"{ content: [{ type: \"text\", text: \"...\" }] }",[27,88577,88578,88579,88582],{},"If something goes wrong, you can signal an error by adding ",[22,88580,88581],{},"isError: true"," to the response:",[128,88584,88586],{"className":13299,"code":88585,"language":13301,"meta":133,"style":133},"return {\n    content: [{ type: \"text\", text: JSON.stringify({ error: \"Item not found\" }) }],\n    isError: true,\n};\n",[22,88587,88588,88594,88618,88627],{"__ignoreMap":133},[137,88589,88590,88592],{"class":139,"line":140},[137,88591,5428],{"class":143},[137,88593,256],{"class":157},[137,88595,88596,88599,88601,88604,88606,88608,88610,88612,88615],{"class":139,"line":173},[137,88597,88598],{"class":157},"    content: [{ type: ",[137,88600,7837],{"class":284},[137,88602,88603],{"class":157},", text: ",[137,88605,22554],{"class":364},[137,88607,1017],{"class":157},[137,88609,24816],{"class":147},[137,88611,50240],{"class":157},[137,88613,88614],{"class":284},"\"Item not found\"",[137,88616,88617],{"class":157}," }) }],\n",[137,88619,88620,88623,88625],{"class":139,"line":188},[137,88621,88622],{"class":157},"    isError: ",[137,88624,3097],{"class":364},[137,88626,1961],{"class":157},[137,88628,88629],{"class":139,"line":269},[137,88630,191],{"class":157},[123,88632,88634],{"id":88633},"other-imperative-api-methods","Other Imperative API Methods",[27,88636,88637,88638,34894,88641,88643],{},"Beyond ",[22,88639,88640],{},"registerTool()",[22,88642,88207],{}," API provides a few more methods:",[1003,88645,88646,88654,88662],{},[1006,88647,88648,88653],{},[42,88649,88650],{},[22,88651,88652],{},"unregisterTool(name)"," - Removes a previously registered tool",[1006,88655,88656,88661],{},[42,88657,88658],{},[22,88659,88660],{},"provideContext(tools)"," - Replaces the entire toolset at once (useful when application state changes)",[1006,88663,88664,88669],{},[42,88665,88666],{},[22,88667,88668],{},"clearContext()"," - Removes all registered tools",[104,88671,88673],{"id":88672},"the-declarative-api","The Declarative API",[27,88675,88676],{},"The Declarative API is the zero-JavaScript approach. If you have an existing HTML form, you can turn it into a WebMCP tool by adding just two attributes:",[128,88678,88680],{"className":4024,"code":88679,"language":4026,"meta":133,"style":133},"\u003Cform\n    toolname=\"reserve_table\"\n    tooldescription=\"Reserve a table at the restaurant\"\n    toolautosubmit\n    action=\"\u002Freservations\"\n>\n    \u003Clabel for=\"date\">Date\u003C\u002Flabel>\n    \u003Cinput type=\"date\" name=\"date\" id=\"date\" required \u002F>\n\n    \u003Clabel for=\"guests\">Number of Guests\u003C\u002Flabel>\n    \u003Cinput type=\"number\" name=\"guests\" id=\"guests\" min=\"1\" max=\"20\" required \u002F>\n\n    \u003Clabel for=\"name\">Your Name\u003C\u002Flabel>\n    \u003Cinput type=\"text\" name=\"name\" id=\"name\" required \u002F>\n\n    \u003Cbutton type=\"submit\">Reserve\u003C\u002Fbutton>\n\u003C\u002Fform>\n",[22,88681,88682,88689,88699,88709,88714,88724,88728,88748,88777,88781,88801,88843,88847,88866,88894,88898,88917],{"__ignoreMap":133},[137,88683,88684,88686],{"class":139,"line":140},[137,88685,4033],{"class":157},[137,88687,88688],{"class":4036},"form\n",[137,88690,88691,88694,88696],{"class":139,"line":173},[137,88692,88693],{"class":147},"    toolname",[137,88695,253],{"class":157},[137,88697,88698],{"class":284},"\"reserve_table\"\n",[137,88700,88701,88704,88706],{"class":139,"line":188},[137,88702,88703],{"class":147},"    tooldescription",[137,88705,253],{"class":157},[137,88707,88708],{"class":284},"\"Reserve a table at the restaurant\"\n",[137,88710,88711],{"class":139,"line":269},[137,88712,88713],{"class":147},"    toolautosubmit\n",[137,88715,88716,88719,88721],{"class":139,"line":278},[137,88717,88718],{"class":147},"    action",[137,88720,253],{"class":157},[137,88722,88723],{"class":284},"\"\u002Freservations\"\n",[137,88725,88726],{"class":139,"line":291},[137,88727,4053],{"class":157},[137,88729,88730,88732,88734,88736,88738,88741,88744,88746],{"class":139,"line":297},[137,88731,4072],{"class":157},[137,88733,23864],{"class":4036},[137,88735,23867],{"class":147},[137,88737,253],{"class":157},[137,88739,88740],{"class":284},"\"date\"",[137,88742,88743],{"class":157},">Date\u003C\u002F",[137,88745,23864],{"class":4036},[137,88747,4053],{"class":157},[137,88749,88750,88752,88754,88756,88758,88760,88762,88764,88766,88768,88770,88772,88775],{"class":139,"line":302},[137,88751,4072],{"class":157},[137,88753,8520],{"class":4036},[137,88755,25639],{"class":147},[137,88757,253],{"class":157},[137,88759,88740],{"class":284},[137,88761,891],{"class":147},[137,88763,253],{"class":157},[137,88765,88740],{"class":284},[137,88767,23757],{"class":147},[137,88769,253],{"class":157},[137,88771,88740],{"class":284},[137,88773,88774],{"class":147}," required",[137,88776,4078],{"class":157},[137,88778,88779],{"class":139,"line":662},[137,88780,516],{"emptyLinePlaceholder":515},[137,88782,88783,88785,88787,88789,88791,88794,88797,88799],{"class":139,"line":667},[137,88784,4072],{"class":157},[137,88786,23864],{"class":4036},[137,88788,23867],{"class":147},[137,88790,253],{"class":157},[137,88792,88793],{"class":284},"\"guests\"",[137,88795,88796],{"class":157},">Number of Guests\u003C\u002F",[137,88798,23864],{"class":4036},[137,88800,4053],{"class":157},[137,88802,88803,88805,88807,88809,88811,88813,88815,88817,88819,88821,88823,88825,88827,88829,88832,88834,88836,88839,88841],{"class":139,"line":786},[137,88804,4072],{"class":157},[137,88806,8520],{"class":4036},[137,88808,25639],{"class":147},[137,88810,253],{"class":157},[137,88812,88385],{"class":284},[137,88814,891],{"class":147},[137,88816,253],{"class":157},[137,88818,88793],{"class":284},[137,88820,23757],{"class":147},[137,88822,253],{"class":157},[137,88824,88793],{"class":284},[137,88826,63659],{"class":147},[137,88828,253],{"class":157},[137,88830,88831],{"class":284},"\"1\"",[137,88833,63675],{"class":147},[137,88835,253],{"class":157},[137,88837,88838],{"class":284},"\"20\"",[137,88840,88774],{"class":147},[137,88842,4078],{"class":157},[137,88844,88845],{"class":139,"line":798},[137,88846,516],{"emptyLinePlaceholder":515},[137,88848,88849,88851,88853,88855,88857,88859,88862,88864],{"class":139,"line":803},[137,88850,4072],{"class":157},[137,88852,23864],{"class":4036},[137,88854,23867],{"class":147},[137,88856,253],{"class":157},[137,88858,5393],{"class":284},[137,88860,88861],{"class":157},">Your Name\u003C\u002F",[137,88863,23864],{"class":4036},[137,88865,4053],{"class":157},[137,88867,88868,88870,88872,88874,88876,88878,88880,88882,88884,88886,88888,88890,88892],{"class":139,"line":931},[137,88869,4072],{"class":157},[137,88871,8520],{"class":4036},[137,88873,25639],{"class":147},[137,88875,253],{"class":157},[137,88877,7837],{"class":284},[137,88879,891],{"class":147},[137,88881,253],{"class":157},[137,88883,5393],{"class":284},[137,88885,23757],{"class":147},[137,88887,253],{"class":157},[137,88889,5393],{"class":284},[137,88891,88774],{"class":147},[137,88893,4078],{"class":157},[137,88895,88896],{"class":139,"line":1568},[137,88897,516],{"emptyLinePlaceholder":515},[137,88899,88900,88902,88904,88906,88908,88910,88913,88915],{"class":139,"line":1573},[137,88901,4072],{"class":157},[137,88903,8170],{"class":4036},[137,88905,25639],{"class":147},[137,88907,253],{"class":157},[137,88909,24528],{"class":284},[137,88911,88912],{"class":157},">Reserve\u003C\u002F",[137,88914,8170],{"class":4036},[137,88916,4053],{"class":157},[137,88918,88919,88921,88923],{"class":139,"line":1578},[137,88920,4083],{"class":157},[137,88922,23831],{"class":4036},[137,88924,4053],{"class":157},[27,88926,88927],{},"The three attributes are:",[1003,88929,88930,88938,88946],{},[1006,88931,88932,88937],{},[42,88933,88934],{},[22,88935,88936],{},"toolname"," - The name of the tool (required). The browser uses this as the tool identifier.",[1006,88939,88940,88945],{},[42,88941,88942],{},[22,88943,88944],{},"tooldescription"," - A natural language description of what the form does (required). The agent reads this to decide when to use it.",[1006,88947,88948,88953],{},[42,88949,88950],{},[22,88951,88952],{},"toolautosubmit"," - When present, the form is automatically submitted after the agent fills in the fields. Without this, the agent fills the form but the user must manually submit it.",[27,88955,88956,88957,88960,88961,88963,88964,164,88967,164,88969,88971],{},"The browser automatically translates the form fields into a JSON Schema based on the ",[22,88958,88959],{},"\u003Cinput>"," types, ",[22,88962,1387],{}," attributes, and validation constraints (",[22,88965,88966],{},"required",[22,88968,64227],{},[22,88970,64215],{},", etc.). No JavaScript needed.",[123,88973,88975],{"id":88974},"css-pseudo-classes-for-agent-interaction","CSS Pseudo-Classes for Agent Interaction",[27,88977,88978],{},"WebMCP also introduces CSS pseudo-classes to let you style forms differently when an agent is interacting with them:",[128,88980,88982],{"className":23162,"code":88981,"language":23164,"meta":133,"style":133},"\u002F* Style the form when an agent is actively filling it *\u002F\nform:tool-form-active {\n    outline: 2px solid #4caf50;\n    background: #f0fff0;\n}\n\n\u002F* Style the submit button when the agent is about to submit *\u002F\nbutton:tool-submit-active {\n    background: #4caf50;\n    color: white;\n}\n",[22,88983,88984,88989,89000,89018,89029,89033,89037,89042,89053,89064,89074],{"__ignoreMap":133},[137,88985,88986],{"class":139,"line":140},[137,88987,88988],{"class":308},"\u002F* Style the form when an agent is actively filling it *\u002F\n",[137,88990,88991,88993,88995,88998],{"class":139,"line":173},[137,88992,23831],{"class":4036},[137,88994,894],{"class":157},[137,88996,88997],{"class":4036},"tool-form-active",[137,88999,256],{"class":157},[137,89001,89002,89005,89007,89009,89011,89013,89016],{"class":139,"line":188},[137,89003,89004],{"class":364},"    outline",[137,89006,726],{"class":157},[137,89008,10345],{"class":364},[137,89010,39722],{"class":143},[137,89012,60815],{"class":364},[137,89014,89015],{"class":364}," #4caf50",[137,89017,3276],{"class":157},[137,89019,89020,89022,89024,89027],{"class":139,"line":269},[137,89021,60405],{"class":364},[137,89023,726],{"class":157},[137,89025,89026],{"class":364},"#f0fff0",[137,89028,3276],{"class":157},[137,89030,89031],{"class":139,"line":278},[137,89032,510],{"class":157},[137,89034,89035],{"class":139,"line":291},[137,89036,516],{"emptyLinePlaceholder":515},[137,89038,89039],{"class":139,"line":297},[137,89040,89041],{"class":308},"\u002F* Style the submit button when the agent is about to submit *\u002F\n",[137,89043,89044,89046,89048,89051],{"class":139,"line":302},[137,89045,8170],{"class":4036},[137,89047,894],{"class":157},[137,89049,89050],{"class":4036},"tool-submit-active",[137,89052,256],{"class":157},[137,89054,89055,89057,89059,89062],{"class":139,"line":662},[137,89056,60405],{"class":364},[137,89058,726],{"class":157},[137,89060,89061],{"class":364},"#4caf50",[137,89063,3276],{"class":157},[137,89065,89066,89068,89070,89072],{"class":139,"line":667},[137,89067,39703],{"class":364},[137,89069,726],{"class":157},[137,89071,60477],{"class":364},[137,89073,3276],{"class":157},[137,89075,89076],{"class":139,"line":786},[137,89077,510],{"class":157},[27,89079,89080],{},"This gives users visual feedback that an AI agent is interacting with the page - a nice touch for transparency.",[123,89082,89084],{"id":89083},"a-note-on-declarative-api-testing","A Note on Declarative API Testing",[3244,89086,89087],{},[27,89088,89089,89090,89093],{},"One important thing to be aware of: the Declarative API (HTML form attributes) currently ",[42,89091,89092],{},"cannot be tested"," with the MCP client methods I'll describe later in this article.",[27,89095,89096,89097,89100,89101,89103,89104,89106,89107,164,89109,89112,89113,89116,89117,114,89119,89121],{},"Both testing methods rely on the ",[22,89098,89099],{},"@mcp-b\u002Fglobal"," polyfill to provide the ",[22,89102,88207],{}," API. This polyfill only implements the ",[42,89105,88248],{}," - it provides ",[22,89108,88640],{},[22,89110,89111],{},"unregisterTool()",", and the other JavaScript methods. It does ",[42,89114,89115],{},"not"," scan the DOM for forms with ",[22,89118,88936],{},[22,89120,88944],{}," attributes.",[27,89123,89124,89125,89128],{},"The form-to-tool translation is a ",[42,89126,89127],{},"native Chrome browser feature",". Chrome itself parses the DOM, detects annotated forms, and auto-registers them as tools behind the scenes. Since the polyfill doesn't do this, only your imperative tools will appear when testing with MCP clients.",[27,89130,89131,89132,89135],{},"The declarative forms will work once Chrome's native WebMCP implementation is fully available - keep an eye on the ",[22,89133,89134],{},"chrome:\u002F\u002Fflags"," \"WebMCP for testing\" flag. In the meantime, I still recommend adding the declarative attributes to your forms now so they're ready when native support lands.",[104,89137,89139],{"id":89138},"setting-up-webmcp-on-your-website","Setting Up WebMCP on Your Website",[27,89141,89142,89143,89146],{},"WebMCP is currently available in ",[42,89144,89145],{},"Chrome 146+"," behind a feature flag. To enable it:",[2569,89148,89149,89155,89161,89167],{},[1006,89150,61028,89151,89154],{},[22,89152,89153],{},"chrome:\u002F\u002Fflags\u002F"," in your browser",[1006,89156,89157,89158],{},"Search for ",[42,89159,89160],{},"\"Experimental Web Platform features\"",[1006,89162,89163,89164],{},"Set it to ",[42,89165,89166],{},"Enabled",[1006,89168,89169],{},"Relaunch Chrome",[27,89171,89172,89173,89177],{},"You can also join the ",[45,89174,89176],{"href":88121,"target":2716,"rel":89175},[2718,2719],"early preview programme"," for access to the full documentation and demos.",[123,89179,89181],{"id":89180},"installing-the-polyfill","Installing the Polyfill",[27,89183,89184,89185,89187,89188,89190],{},"Since WebMCP is still behind a flag and not available in all browsers, you'll want to use the ",[22,89186,89099],{}," polyfill. This package provides ",[22,89189,88207],{}," for browsers that don't natively support it yet.",[128,89192,89194],{"className":8665,"code":89193,"language":8667,"meta":133,"style":133},"npm install @mcp-b\u002Fglobal\n",[22,89195,89196],{"__ignoreMap":133},[137,89197,89198,89200,89202],{"class":139,"line":140},[137,89199,9536],{"class":147},[137,89201,10268],{"class":284},[137,89203,89204],{"class":284}," @mcp-b\u002Fglobal\n",[27,89206,89207],{},"Then import it before registering your tools:",[128,89209,89211],{"className":13299,"code":89210,"language":13301,"meta":133,"style":133},"import \"@mcp-b\u002Fglobal\";\n\n\u002F\u002F Now navigator.modelContext is available\nnavigator.modelContext.registerTool({\n    name: \"my_tool\",\n    description: \"Does something useful\",\n    inputSchema: { type: \"object\", properties: {} },\n    execute: async () => {\n        return {\n            content: [{ type: \"text\", text: \"Hello from my tool!\" }],\n        };\n    },\n});\n",[22,89212,89213,89222,89226,89231,89239,89248,89257,89267,89281,89287,89301,89305,89309],{"__ignoreMap":133},[137,89214,89215,89217,89220],{"class":139,"line":140},[137,89216,10287],{"class":143},[137,89218,89219],{"class":284}," \"@mcp-b\u002Fglobal\"",[137,89221,3276],{"class":157},[137,89223,89224],{"class":139,"line":173},[137,89225,516],{"emptyLinePlaceholder":515},[137,89227,89228],{"class":139,"line":188},[137,89229,89230],{"class":308},"\u002F\u002F Now navigator.modelContext is available\n",[137,89232,89233,89235,89237],{"class":139,"line":269},[137,89234,88277],{"class":157},[137,89236,88280],{"class":147},[137,89238,3175],{"class":157},[137,89240,89241,89243,89246],{"class":139,"line":278},[137,89242,57427],{"class":157},[137,89244,89245],{"class":284},"\"my_tool\"",[137,89247,1961],{"class":157},[137,89249,89250,89252,89255],{"class":139,"line":291},[137,89251,88296],{"class":157},[137,89253,89254],{"class":284},"\"Does something useful\"",[137,89256,1961],{"class":157},[137,89258,89259,89262,89264],{"class":139,"line":297},[137,89260,89261],{"class":157},"    inputSchema: { type: ",[137,89263,83873],{"class":284},[137,89265,89266],{"class":157},", properties: {} },\n",[137,89268,89269,89271,89273,89275,89277,89279],{"class":139,"line":302},[137,89270,88413],{"class":147},[137,89272,726],{"class":157},[137,89274,15050],{"class":143},[137,89276,1484],{"class":157},[137,89278,222],{"class":143},[137,89280,256],{"class":157},[137,89282,89283,89285],{"class":139,"line":662},[137,89284,5472],{"class":143},[137,89286,256],{"class":157},[137,89288,89289,89292,89294,89296,89299],{"class":139,"line":667},[137,89290,89291],{"class":157},"            content: [{ type: ",[137,89293,7837],{"class":284},[137,89295,88603],{"class":157},[137,89297,89298],{"class":284},"\"Hello from my tool!\"",[137,89300,21745],{"class":157},[137,89302,89303],{"class":139,"line":786},[137,89304,1507],{"class":157},[137,89306,89307],{"class":139,"line":798},[137,89308,775],{"class":157},[137,89310,89311],{"class":139,"line":803},[137,89312,5422],{"class":157},[27,89314,89315],{},"The polyfill automatically detects whether native browser support exists and only activates when needed. So your code will work in both polyfilled and native environments.",[104,89317,89319],{"id":89318},"testing-your-webmcp-tools","Testing Your WebMCP Tools",[27,89321,89322],{},"Now here's the fun part - actually testing your tools with real AI agents. I tested two methods, both of which work brilliantly.",[123,89324,89326,89327,89330],{"id":89325},"method-1-mcp-bchrome-devtools-mcp-recommended","Method 1: ",[22,89328,89329],{},"@mcp-b\u002Fchrome-devtools-mcp"," (Recommended)",[27,89332,89333,89334,89340,89341,114,89344,89347,89348,1017],{},"This is the method I recommend and the one I use myself. ",[45,89335,89338],{"href":89336,"target":2716,"rel":89337},"https:\u002F\u002Fdocs.mcp-b.ai\u002Fpackages\u002Fchrome-devtools-mcp",[2718,2719],[42,89339,89329],{}," is an MCP server that connects your AI client (VS Code, Claude Desktop, Claude Code, Cursor) to a live Chrome browser. It provides two WebMCP-specific tools: ",[22,89342,89343],{},"list_webmcp_tools",[22,89345,89346],{},"call_webmcp_tool",", which let your AI agent discover and call any tools registered via ",[22,89349,88207],{},[3244,89351,89352],{},[27,89353,89354,89357,89358,89361,89362,89364,89365,89367],{},[42,89355,89356],{},"Important:"," This is different from Google's official ",[22,89359,89360],{},"chrome-devtools-mcp",". Google's version handles browser automation (screenshots, navigation, script evaluation) but does ",[42,89363,89115],{}," support WebMCP tool discovery. Make sure you use ",[22,89366,89329],{}," (the MCP-B version) for WebMCP testing.",[27,89369,89370],{},"Here's how to set it up for different clients:",[27,89372,89373,89375],{},[42,89374,22693],{}," (my preferred setup):",[128,89377,89379],{"className":8665,"code":89378,"language":8667,"meta":133,"style":133},"code --add-mcp '{\"name\":\"chrome-devtools\",\"command\":\"npx\",\"args\":[\"-y\",\"@mcp-b\u002Fchrome-devtools-mcp@latest\"]}'\n",[22,89380,89381],{"__ignoreMap":133},[137,89382,89383,89385,89388],{"class":139,"line":140},[137,89384,22],{"class":147},[137,89386,89387],{"class":364}," --add-mcp",[137,89389,89390],{"class":284}," '{\"name\":\"chrome-devtools\",\"command\":\"npx\",\"args\":[\"-y\",\"@mcp-b\u002Fchrome-devtools-mcp@latest\"]}'\n",[27,89392,89393],{},[42,89394,89395],{},"Claude Code:",[128,89397,89399],{"className":8665,"code":89398,"language":8667,"meta":133,"style":133},"claude mcp add chrome-devtools npx @mcp-b\u002Fchrome-devtools-mcp@latest\n",[22,89400,89401],{"__ignoreMap":133},[137,89402,89403,89406,89409,89411,89414,89417],{"class":139,"line":140},[137,89404,89405],{"class":147},"claude",[137,89407,89408],{"class":284}," mcp",[137,89410,17266],{"class":284},[137,89412,89413],{"class":284}," chrome-devtools",[137,89415,89416],{"class":284}," npx",[137,89418,89419],{"class":284}," @mcp-b\u002Fchrome-devtools-mcp@latest\n",[27,89421,89422,89425,89426,70468],{},[42,89423,89424],{},"Claude Desktop"," (add to ",[22,89427,89428],{},"claude_desktop_config.json",[128,89430,89432],{"className":5155,"code":89431,"language":5157,"meta":133,"style":133},"{\n    \"mcpServers\": {\n        \"chrome-devtools\": {\n            \"command\": \"npx\",\n            \"args\": [\"-y\", \"@mcp-b\u002Fchrome-devtools-mcp@latest\"]\n        }\n    }\n}\n",[22,89433,89434,89438,89445,89452,89464,89480,89484,89488],{"__ignoreMap":133},[137,89435,89436],{"class":139,"line":140},[137,89437,15971],{"class":157},[137,89439,89440,89443],{"class":139,"line":173},[137,89441,89442],{"class":364},"    \"mcpServers\"",[137,89444,1819],{"class":157},[137,89446,89447,89450],{"class":139,"line":188},[137,89448,89449],{"class":364},"        \"chrome-devtools\"",[137,89451,1819],{"class":157},[137,89453,89454,89457,89459,89462],{"class":139,"line":269},[137,89455,89456],{"class":364},"            \"command\"",[137,89458,726],{"class":157},[137,89460,89461],{"class":284},"\"npx\"",[137,89463,1961],{"class":157},[137,89465,89466,89469,89471,89473,89475,89478],{"class":139,"line":278},[137,89467,89468],{"class":364},"            \"args\"",[137,89470,29669],{"class":157},[137,89472,86019],{"class":284},[137,89474,164],{"class":157},[137,89476,89477],{"class":284},"\"@mcp-b\u002Fchrome-devtools-mcp@latest\"",[137,89479,33307],{"class":157},[137,89481,89482],{"class":139,"line":291},[137,89483,1966],{"class":157},[137,89485,89486],{"class":139,"line":297},[137,89487,294],{"class":157},[137,89489,89490],{"class":139,"line":302},[137,89491,510],{"class":157},[27,89493,89494,89497,89498,1017],{},[42,89495,89496],{},"Cursor:"," To find the direct installer to set up Cursor, go to the ",[45,89499,89502],{"href":89500,"target":2716,"rel":89501},"https:\u002F\u002Fdocs.mcp-b.ai\u002Fpackages\u002Fchrome-devtools-mcp#cursor",[2718,2719],"mcp-b documentation here",[27,89504,89505],{},"Once set up, start your development server, open your site in Chrome, and ask your AI agent to interact with it.",[27,89507,89508],{},"Here is a conversation example I did after setting up my VS Code using Claude Sonnet 4.6:",[27,89510,89511,3596,89514],{},[42,89512,89513],{},"Me:",[30,89515,89516,89517,89525,89530],{},"\"Navigate to ",[45,89518,89521,89524],{"href":89519,"target":2716,"rel":89520},"https:\u002F\u002Fwww.trpkovski.com",[2718,2719],[137,89522,89523],{},"h","ttps:\u002F\u002F",[45,89526,89529],{"href":89527,"rel":89528},"http:\u002F\u002Fwww.trpkovski.com",[10924],"www.trpkovski.com",", list available WebMCP tools\"",[27,89532,89533,89536],{},[42,89534,89535],{},"Agent:"," Found 6 available WebMCP tools:",[45740,89538,89539,89547],{},[45743,89540,89541],{},[45746,89542,89543,89545],{},[45749,89544,85845],{},[45749,89546,56463],{},[45762,89548,89549,89557,89565,89573,89581,89589],{},[45746,89550,89551,89554],{},[45767,89552,89553],{},"search_articles",[45767,89555,89556],{},"Search blog articles by keyword and\u002For category",[45746,89558,89559,89562],{},[45767,89560,89561],{},"get_article",[45767,89563,89564],{},"Get full details of a specific article by path",[45746,89566,89567,89570],{},[45767,89568,89569],{},"list_categories",[45767,89571,89572],{},"List all available blog categories",[45746,89574,89575,89578],{},[45767,89576,89577],{},"get_recent_articles",[45767,89579,89580],{},"Get the most recently published articles (up to 10)",[45746,89582,89583,89586],{},[45767,89584,89585],{},"get_similar_articles",[45767,89587,89588],{},"Find articles similar to a given one using AI semantic similarity",[45746,89590,89591,89594],{},[45767,89592,89593],{},"navigate_to",[45767,89595,89596],{},"Navigate to a specific page on the blog",[27,89598,89599,3596,89601],{},[42,89600,89513],{},[30,89602,89603],{},"\"Can you help me find any article about Nest.js with Firebase Auth\"",[27,89605,89606,89608,89609,89611,89612,89614],{},[42,89607,89535],{}," Found it! There's a directly relevant article: ",[42,89610,16640],{}," - Published: 7th October 2022 · 15 min read. Tags: Nest.js, Firebase, Node.js. There's also a follow-up: ",[42,89613,19586],{}," - Published: 10th October 2022 · 5 min read.",[27,89616,89617,3596,89619],{},[42,89618,89513],{},[30,89620,89621],{},"\"Is there any similar articles to Nest.js Authorisation with Firebase Auth\"",[27,89623,89624,89626,89627,89629,89630,89632,89633,89635],{},[42,89625,89535],{}," Here are 3 similar articles: ",[42,89628,12814],{}," (3rd Oct 2022, 16 min), ",[42,89631,19586],{}," (10th Oct 2022, 5 min), and ",[42,89634,2666],{}," (13th Jul 2021, 9 min).",[27,89637,89638,89639,89641,89642,89644],{},"The agent discovered the tools, called ",[22,89640,89553],{}," to find the Nest.js article, and then called ",[22,89643,89585],{}," to find related content - all through structured tool calls, no screen-scraping involved.",[27,89646,89647,89648,89653,89656],{},"You can also try this yourself on my blog at ",[45,89649,89651,89524],{"href":89519,"target":2716,"rel":89650},[2718,2719],[137,89652,89523],{},[45,89654,89529],{"href":89527,"rel":89655},[10924]," - once you have your client set up, go ahead and explore the tools.",[123,89658,89660],{"id":89659},"method-2-mcp-b-browser-extension-alternative","Method 2: MCP-B Browser Extension (Alternative)",[27,89662,89663,89664,89671],{},"If you prefer testing directly in the browser without setting up an external AI client, the ",[45,89665,89668],{"href":89666,"target":2716,"rel":89667},"https:\u002F\u002Fchromewebstore.google.com\u002Fdetail\u002Fmcp-b-extension\u002Fdaohopfhkdelnpemnhlekblhnikhdhfa",[2718,2719],[42,89669,89670],{},"MCP-B Browser Extension"," is a great alternative. Install it from the Chrome Web Store, click the icon in your toolbar, and it opens a sidebar with an AI agent that automatically discovers WebMCP tools from all your open tabs.",[27,89673,89674,89675,1017],{},"You can read more about it in the ",[45,89676,89679],{"href":89677,"target":2716,"rel":89678},"https:\u002F\u002Fdocs.mcp-b.ai\u002Fextension",[2718,2719],"MCP-B Extension documentation",[104,89681,89683],{"id":89682},"adding-webmcp-to-my-blog","Adding WebMCP to My Blog",[27,89685,89686],{},"To put all of this into practice, I added WebMCP to this blog. The goal was to let AI agents search articles, explore categories, find related content, and navigate the site - all through structured tools.",[27,89688,89689,89690,89693,89694,89697],{},"I built ",[42,89691,89692],{},"six imperative tools"," and added ",[42,89695,89696],{},"declarative attributes"," to two existing forms. Here's what each tool does:",[123,89699,89553],{"id":89553},[27,89701,89702],{},"Searches blog posts by keyword and\u002For category tag. The keyword matches against article titles, descriptions, and keywords. The category filter matches against article tags. Both parameters are optional - you can search by keyword only, category only, or both. Returns the count of matching articles along with their title, path, description, published date, read time, author, tags, and image.",[123,89704,89561],{"id":89561},[27,89706,89707,89708,89711],{},"Gets the full details of a specific article by its path. You pass in the article path (e.g. ",[22,89709,89710],{},"\u002F2024\u002F01\u002F15\u002Fmy-article-slug",") and it returns the article's title, description, image URL, tags, published date, read time, and author. Useful when the agent already knows which article it wants to inspect.",[123,89713,89569],{"id":89569},[27,89715,89716],{},"Lists all 25 blog categories with the number of articles in each. Takes no parameters. Returns an array of categories, each with a display name, URL path, and article count. This helps the agent understand what topics the blog covers before searching.",[123,89718,89577],{"id":89577},[27,89720,89721,89722,89724],{},"Returns the most recently published blog posts. Accepts an optional ",[22,89723,40296],{}," parameter (defaults to 5, maximum 10). Useful for agents that want to know what's new on the blog without searching for a specific topic.",[123,89726,89585],{"id":89585},[27,89728,89729],{},"Finds articles semantically similar to a given article using AI-powered embeddings. You pass in an article path and it returns the 3 most related articles. Under the hood, it calls the blog's existing similarity API, which uses pre-generated vector embeddings and cosine similarity to find the closest matches.",[123,89731,89593],{"id":89593},[27,89733,89734,89735,114,89738,89741],{},"Navigates the browser to a specific page on the blog. Accepts any path - article pages, category listings, or special pages like ",[22,89736,89737],{},"\u002Fabout-me",[22,89739,89740],{},"\u002Fget-in-touch",". Returns a confirmation of the navigation. This lets the agent move the user to a relevant page after finding what they're looking for.",[123,89743,89745],{"id":89744},"declarative-forms","Declarative Forms",[27,89747,89748,89749,164,89751,14528,89753,89755,89756,89759,89760,89763,89764,89759,89767,89770],{},"I also added ",[22,89750,88936],{},[22,89752,88944],{},[22,89754,88952],{}," attributes to the ",[42,89757,89758],{},"newsletter subscription"," form (",[22,89761,89762],{},"subscribe_newsletter",") and the ",[42,89765,89766],{},"contact",[22,89768,89769],{},"send_message","). These are ready for when Chrome's native Declarative API support is fully available.",[27,89772,89773,89774,1017],{},"You can find the full WebMCP integration - all tool definitions, the client plugin, and the status component - in the ",[45,89775,89777],{"href":47632,"target":2716,"rel":89776},[2718,2719],"blog's repository",[104,89779,82351],{"id":82350},[27,89781,89782],{},"WebMCP is still in early preview, and things will evolve. But the direction is clear: the web is becoming a first-class platform for AI agent interaction. Instead of agents clumsily navigating websites the way a human would - clicking, scrolling, reading pixels - websites themselves will tell agents exactly what they can do.",[27,89784,89785,89786,89789,89790,89793],{},"What excites me most is the long-term vision. If WebMCP adoption grows and most websites start exposing structured tools, the way we interact with the web could fundamentally change. You wouldn't need to visit ten different airline websites to compare flight prices. You'd just ask your agent: ",[30,89787,89788],{},"\"Find me the cheapest flight to London next Friday\""," - and it would call ",[22,89791,89792],{},"search_flights"," on each airline's site, get structured results back, and give you a clean comparison in seconds.",[27,89795,89796],{},"We're still early, but this is the kind of infrastructure shift that makes the agentic web possible. If you build websites, I'd encourage you to start experimenting with WebMCP now. Add a few tools, test them with the methods I've described, and see how it feels. The polyfill makes it easy to get started today, and your tools will work natively once browser support matures.",[2617,89798,89799],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":133,"searchDepth":173,"depth":173,"links":89801},[89802,89803,89804,89807,89811,89814,89819,89828],{"id":88143,"depth":173,"text":88144},{"id":88196,"depth":173,"text":88197},{"id":88256,"depth":173,"text":88257,"children":89805},[89806],{"id":88633,"depth":188,"text":88634},{"id":88672,"depth":173,"text":88673,"children":89808},[89809,89810],{"id":88974,"depth":188,"text":88975},{"id":89083,"depth":188,"text":89084},{"id":89138,"depth":173,"text":89139,"children":89812},[89813],{"id":89180,"depth":188,"text":89181},{"id":89318,"depth":173,"text":89319,"children":89815},[89816,89818],{"id":89325,"depth":188,"text":89817},"Method 1: @mcp-b\u002Fchrome-devtools-mcp (Recommended)",{"id":89659,"depth":188,"text":89660},{"id":89682,"depth":173,"text":89683,"children":89820},[89821,89822,89823,89824,89825,89826,89827],{"id":89553,"depth":188,"text":89553},{"id":89561,"depth":188,"text":89561},{"id":89569,"depth":188,"text":89569},{"id":89577,"depth":188,"text":89577},{"id":89585,"depth":188,"text":89585},{"id":89593,"depth":188,"text":89593},{"id":89744,"depth":188,"text":89745},{"id":82350,"depth":173,"text":82351},"WebMCP is a new browser standard that lets websites expose structured tools for AI agents. Learn how it works, how to implement both the Imperative and Declarative APIs, and how to test your WebMCP tools using real MCP clients like VS Code, Claude Desktop, and more.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1772448782\u002Fblog\u002Fmcp-is-coming-to-the-browser-webmcp-and-the-future-of-ai-powered-websites\u002Fhero-mcp-is-coming-to-the-browser_ntmegq",[88125,89832,89833,72676,71432,89834,88207,89835,89836,89837,89838,88936,88944,89360,89839,89840,2655,9,12817],"Web Model Context Protocol","MCP","browser AI","Chrome 146","mcp-b\u002Fglobal","imperative API","declarative API","AI browser tools","structured tools",{},"\u002F2026\u002F03\u002F08\u002Fmcp-is-coming-to-the-browser","8th March 2026",{"title":88081,"description":89829},"2026\u002F03\u002F08\u002Fmcp-is-coming-to-the-browser","ycS4f-rdaJOfmQm6sjLJt81gF1X-BXClj5fnLVgt_Zs",{"id":89848,"title":89849,"articleTags":89850,"author":11,"blog":12,"body":89851,"description":93309,"extension":2649,"image":93310,"keywords":93311,"meta":93324,"navigation":515,"path":93325,"published":93326,"readTime":931,"seo":93327,"stem":93328,"type":2662,"__hash__":93329},"content\u002F2026\u002F03\u002F15\u002Fhow-to-create-a-nuxt-module-a-beginner-friendly-guide.md","How to Create a Nuxt Module: A Beginner-Friendly Guide",[21096,8,12817],{"type":14,"value":89852,"toc":93274},[89853,89856,89870,89872,89876,89881,89898,89908,89911,89915,89922,89925,89931,89935,89938,89955,89986,89997,89999,90013,90035,90048,90056,90058,90061,90076,90079,90083,90086,90092,90110,90116,90123,90136,90140,90143,90149,90152,90169,90178,90182,90200,90455,90458,90536,90543,90547,90559,91088,91091,91097,91156,91180,91230,91240,91246,91291,91304,91309,91315,91331,91336,91341,91379,91393,91398,91435,91454,91459,91531,91544,91550,91588,91593,91603,91606,91610,91616,91853,91856,91881,91892,91907,91939,91943,91950,91956,91959,92433,92436,92456,92468,92487,92490,92579,92588,92594,92597,92702,92708,92814,92818,92825,92884,92952,92962,92966,92973,93023,93032,93036,93039,93099,93103,93106,93113,93124,93131,93135,93141,93144,93153,93159,93163,93169,93202,93208,93211,93214,93218,93221,93227,93229,93250,93253,93256,93271],[17,89854,89849],{"id":89855},"how-to-create-a-nuxt-module-a-beginner-friendly-guide",[27,89857,89858],{},[30,89859,89860,36,89862,88096,89864],{},[33,89861],{"value":35},[33,89863],{"value":39},[42,89865,89866],{},[45,89867,89868],{"href":47},[33,89869],{"value":50},[52,89871],{":tags":54},[56,89873],{":audio-src":89874,":transcript-src":89875},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F03\u002F15\u002Fhow-to-create-a-nuxt-module-a-beginner-friendly-guide\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F03\u002F15\u002Fhow-to-create-a-nuxt-module-a-beginner-friendly-guide\u002Fsummary.json",[27,89877,89878],{},[63,89879],{"alt":12847,"src":89880},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1773227493\u002Fblog\u002Fhow-to-create-a-nuxt-module-a-beginner-friendly-guide\u002Fhow-to-create-a-nuxt-module-a-beginner-friendly-guide-2_kn1xkg",[27,89882,89883,89884,164,89887,164,89890,89893,89894,89897],{},"If you've used Nuxt for any length of time, you've probably installed a few modules - ",[22,89885,89886],{},"@nuxtjs\u002Ftailwindcss",[22,89888,89889],{},"@pinia\u002Fnuxt",[22,89891,89892],{},"nuxt-icon",". You add one line to your ",[22,89895,89896],{},"modules"," array, and everything just works: auto-imports, configuration, SSR handling, the lot. But have you ever looked inside one of those modules and wondered how they actually work? Or thought about building your own?",[27,89899,89900,89901,89907],{},"I had that exact thought recently. I needed to integrate a JavaScript library called ",[45,89902,89905],{"href":89903,"target":2716,"rel":89904},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@mcp-b\u002Fglobal",[2718,2719],[22,89906,89099],{}," into a Nuxt project, and there was no existing module for it. So I built one. The process taught me that Nuxt module development is far more approachable than it looks from the outside - and I want to share what I learned.",[27,89909,89910],{},"In this article, we'll build a real, publishable Nuxt module together from scratch. Not a toy example - an actual module that's live on npm right now. By the end, you'll understand every piece of the architecture and feel confident building your own.",[104,89912,89914],{"id":89913},"why-i-wrote-this","Why I Wrote This",[27,89916,89917,89918,89921],{},"I've been following Vue.js and Nuxt.js very closely for several years now. If you're coming from the React world, the simplest way to think about it is this: ",[42,89919,89920],{},"Nuxt is to Vue what Next.js is to React"," - but in my experience, it's even better. The developer experience is remarkably polished, the community is great, and the framework stays out of your way while giving you everything you need.",[27,89923,89924],{},"What keeps me coming back, though, is the community. The Vue and Nuxt communities are some of the friendliest and most welcoming I've encountered in the JavaScript ecosystem. Whether you're asking a question on Discord, posting on Bluesky or X, opening an issue on GitHub, or browsing through the docs, you'll find people who genuinely want to help. That warmth has made a real difference in how I learn and build.",[27,89926,89927,89928,89930],{},"When I realised there was no Nuxt module for ",[22,89929,89099],{},", I didn't just want to build one for myself - I wanted to document the process so others could learn from it too.",[104,89932,89934],{"id":89933},"a-quick-word-on-vue-and-nuxt","A Quick Word on Vue and Nuxt",[27,89936,89937],{},"If you're not familiar with the ecosystem yet, here's the short version.",[27,89939,89940,89946,89947,158,89952,89954],{},[45,89941,89944],{"href":89942,"target":2716,"rel":89943},"https:\u002F\u002Fvuejs.org\u002F",[2718,2719],[42,89945,8],{}," is a progressive JavaScript framework for building user interfaces. It's known for its gentle learning curve, excellent documentation, and a reactivity system that feels intuitive from day one. You write components using a ",[45,89948,89951],{"href":89949,"target":2716,"rel":89950},"https:\u002F\u002Fvuejs.org\u002Fguide\u002Fscaling-up\u002Fsfc.html",[2718,2719],"single-file format",[22,89953,7575],{}," files) that keeps your template, script, and styles together in one place.",[27,89956,89957,89963,89964,164,89969,164,89974,89979,89980,89985],{},[45,89958,89960],{"href":30780,"target":2716,"rel":89959},[2718,2719],[42,89961,89962],{},"Nuxt"," is a full-stack framework built on top of Vue. It adds ",[45,89965,89968],{"href":89966,"target":2716,"rel":89967},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Fconcepts\u002Frendering#universal-rendering",[2718,2719],"server-side rendering",[45,89970,89973],{"href":89971,"target":2716,"rel":89972},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fgetting-started\u002Frouting",[2718,2719],"file-based routing",[45,89975,89978],{"href":89976,"target":2716,"rel":89977},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Fconcepts\u002Fauto-imports",[2718,2719],"auto-imports",", API routes, and a powerful ",[45,89981,89984],{"href":89982,"target":2716,"rel":89983},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Fconcepts\u002Fmodules",[2718,2719],"module system",". Think of Vue as the engine and Nuxt as the car - Vue gives you the reactivity and component model, and Nuxt gives you everything else you need to build and deploy a production application.",[27,89987,4737,89988,89990,89991,89996],{},[42,89989,89984],{}," is one of Nuxt's most powerful features. Modules are functions that run when Nuxt starts up. They can register plugins, add composables, inject CSS, extend the build configuration, add server routes - essentially anything you might need to integrate a library or add functionality to your application. The Nuxt ecosystem has ",[45,89992,89995],{"href":89993,"target":2716,"rel":89994},"https:\u002F\u002Fnuxt.com\u002Fmodules",[2718,2719],"hundreds of community modules"," that you can drop into your project with a single line of configuration.",[104,89998,59181],{"id":59180},[27,90000,90001,90002,90007,90008,90012],{},"We're going to wrap ",[45,90003,90005],{"href":89903,"target":2716,"rel":90004},[2718,2719],[22,90006,89099],{}," - a ",[45,90009,90011],{"href":88129,"target":2716,"rel":90010},[2718,2719],"W3C Web Model Context API"," polyfill that lets AI agents interact with your website - into a Nuxt module. The finished module will:",[2569,90014,90015,90018,90027,90032],{},[1006,90016,90017],{},"Auto-initialise the library on the client side",[1006,90019,90020,90021,114,90024,14105],{},"Provide auto-imported composables (",[22,90022,90023],{},"useMcpTool",[22,90025,90026],{},"useMcpB",[1006,90028,90029,90030],{},"Expose configuration options through ",[22,90031,21711],{},[1006,90033,90034],{},"Include full TypeScript support",[27,90036,90037,90038,90040,90041,90044,90045,90047],{},"The key thing about ",[22,90039,89099],{}," is that it's a ",[42,90042,90043],{},"client-side library",". It polyfills ",[22,90046,88207],{}," in the browser so AI agents can discover and call tools on your website. This means our module needs to be careful about only running code in the browser - never during server-side rendering.",[3244,90049,90050],{},[27,90051,90052,90053,90055],{},"You don't need to know anything about ",[22,90054,89099],{}," to follow along. The patterns we'll use apply to wrapping any JavaScript library into a Nuxt module.",[104,90057,66831],{"id":66828},[27,90059,90060],{},"Before we start, make sure you have:",[1003,90062,90063,90068],{},[1006,90064,90065,90067],{},[42,90066,2669],{}," 24 or later",[1006,90069,90070,164,90072,29088,90074],{},[42,90071,9536],{},[42,90073,17263],{},[42,90075,29087],{},[27,90077,90078],{},"That's it. No special tooling required.",[104,90080,90082],{"id":90081},"understanding-the-two-worlds","Understanding the Two Worlds",[27,90084,90085],{},"Before we write any code, there's one concept that trips up almost everyone new to Nuxt module development. I want to explain it clearly because everything else builds on it.",[27,90087,90088,90089,894],{},"A Nuxt module has ",[42,90090,90091],{},"two separate worlds",[2569,90093,90094,90104],{},[1006,90095,90096,90099,90100,90103],{},[42,90097,90098],{},"Build time"," - code that runs when Nuxt starts up (your ",[22,90101,90102],{},"module.ts"," file)",[1006,90105,90106,90109],{},[42,90107,90108],{},"Application time"," - code that runs inside the user's actual application (plugins, composables, components)",[27,90111,90112,90113,90115],{},"Your module definition (",[22,90114,90102],{},") runs at build time. It tells Nuxt: \"Hey, register this plugin, add these composables, set up this configuration.\" It's like a set of instructions.",[27,90117,90118,90119,90122],{},"The files in the ",[22,90120,90121],{},"runtime\u002F"," directory run at application time - in the user's browser or on their server. These are the plugins that initialise libraries, the composables that users call in their components, and any components your module provides.",[27,90124,90125,90126,90135],{},"These two worlds can't directly share variables or state. If your module receives configuration options at build time and your plugin needs them at application time, you need a bridge. That bridge is ",[45,90127,90130],{"href":90128,"target":2716,"rel":90129},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Fgoing-further\u002Fruntime-config",[2718,2719],[42,90131,90132],{},[22,90133,90134],{},"runtimeConfig"," - and we'll see exactly how it works shortly.",[104,90137,90139],{"id":90138},"the-module-structure","The Module Structure",[27,90141,90142],{},"Here's what our finished module looks like:",[128,90144,90147],{"className":90145,"code":90146,"language":5189},[5187],"nuxt-mcp-b\u002F\n├── src\u002F\n│   ├── module.ts                         # Module definition (build time)\n│   ├── types.ts                          # Exported types\n│   └── runtime\u002F\n│       └── app\u002F\n│           ├── plugins\u002F\n│           │   └── mcp-b.client.ts       # Client-only plugin (application time)\n│           └── composables\u002F\n│               ├── useMcpB.ts            # Manual init\u002Fcleanup composable\n│               └── useMcpTool.ts         # Tool registration composable\n├── playground\u002F\n│   ├── nuxt.config.ts                    # Dev playground config\n│   └── app.vue                           # Demo page\n├── package.json\n└── tsconfig.json\n",[22,90148,90146],{"__ignoreMap":133},[27,90150,90151],{},"Two key directories to understand:",[1003,90153,90154,90161],{},[1006,90155,90156,90160],{},[42,90157,90158],{},[22,90159,13088],{}," contains the module definition and all the code that ships to users",[1006,90162,90163,90168],{},[42,90164,90165],{},[22,90166,90167],{},"src\u002Fruntime\u002F"," contains the code that runs inside the user's Nuxt application (plugins, composables, components)",[27,90170,90171,90172,90174,90175,90177],{},"This separation mirrors the two worlds I described above. ",[22,90173,90102],{}," is build time. Everything in ",[22,90176,90121],{}," is application time.",[104,90179,90181],{"id":90180},"step-1-setting-up-the-project","Step 1: Setting Up the Project",[27,90183,90184,90185,90187,90188,34894,90190,90193,90194,114,90197,1017],{},"Let's start by creating the ",[22,90186,5140],{},". The key fields for a Nuxt module are the ",[22,90189,8756],{},[22,90191,90192],{},"prepack"," script (which builds the module), and the dependencies on ",[22,90195,90196],{},"@nuxt\u002Fkit",[22,90198,90199],{},"@nuxt\u002Fmodule-builder",[128,90201,90203],{"className":5155,"code":90202,"language":5157,"meta":133,"style":133},"{\n    \"name\": \"nuxt-mcp-b\",\n    \"version\": \"0.1.0\",\n    \"description\": \"Nuxt module for @mcp-b\u002Fglobal\",\n    \"license\": \"MIT\",\n    \"type\": \"module\",\n    \"exports\": {\n        \".\": {\n            \"import\": \".\u002Fdist\u002Fmodule.mjs\"\n        }\n    },\n    \"main\": \".\u002Fdist\u002Fmodule.mjs\",\n    \"files\": [\"dist\"],\n    \"scripts\": {\n        \"prepack\": \"nuxt-module-build build\",\n        \"dev\": \"npx nuxi dev playground\",\n        \"dev:prepare\": \"nuxt-module-build build --stub && nuxt-module-build prepare && npx nuxi prepare playground\"\n    },\n    \"dependencies\": {\n        \"@mcp-b\u002Fglobal\": \"^2.0.13\",\n        \"@nuxt\u002Fkit\": \"^4.3.1\"\n    },\n    \"devDependencies\": {\n        \"@nuxt\u002Fmodule-builder\": \"^1.0.2\",\n        \"@nuxt\u002Fschema\": \"^4.3.1\",\n        \"nuxt\": \"^4.3.1\",\n        \"typescript\": \"^5.9.3\"\n    }\n}\n",[22,90204,90205,90209,90220,90231,90243,90255,90265,90272,90279,90289,90293,90297,90309,90321,90327,90339,90350,90360,90364,90370,90382,90392,90396,90402,90414,90426,90437,90447,90451],{"__ignoreMap":133},[137,90206,90207],{"class":139,"line":140},[137,90208,15971],{"class":157},[137,90210,90211,90213,90215,90218],{"class":139,"line":173},[137,90212,79806],{"class":364},[137,90214,726],{"class":157},[137,90216,90217],{"class":284},"\"nuxt-mcp-b\"",[137,90219,1961],{"class":157},[137,90221,90222,90224,90226,90229],{"class":139,"line":188},[137,90223,79818],{"class":364},[137,90225,726],{"class":157},[137,90227,90228],{"class":284},"\"0.1.0\"",[137,90230,1961],{"class":157},[137,90232,90233,90236,90238,90241],{"class":139,"line":269},[137,90234,90235],{"class":364},"    \"description\"",[137,90237,726],{"class":157},[137,90239,90240],{"class":284},"\"Nuxt module for @mcp-b\u002Fglobal\"",[137,90242,1961],{"class":157},[137,90244,90245,90248,90250,90253],{"class":139,"line":278},[137,90246,90247],{"class":364},"    \"license\"",[137,90249,726],{"class":157},[137,90251,90252],{"class":284},"\"MIT\"",[137,90254,1961],{"class":157},[137,90256,90257,90259,90261,90263],{"class":139,"line":291},[137,90258,57339],{"class":364},[137,90260,726],{"class":157},[137,90262,25777],{"class":284},[137,90264,1961],{"class":157},[137,90266,90267,90270],{"class":139,"line":297},[137,90268,90269],{"class":364},"    \"exports\"",[137,90271,1819],{"class":157},[137,90273,90274,90277],{"class":139,"line":302},[137,90275,90276],{"class":364},"        \".\"",[137,90278,1819],{"class":157},[137,90280,90281,90284,90286],{"class":139,"line":662},[137,90282,90283],{"class":364},"            \"import\"",[137,90285,726],{"class":157},[137,90287,90288],{"class":284},"\".\u002Fdist\u002Fmodule.mjs\"\n",[137,90290,90291],{"class":139,"line":667},[137,90292,1966],{"class":157},[137,90294,90295],{"class":139,"line":786},[137,90296,775],{"class":157},[137,90298,90299,90302,90304,90307],{"class":139,"line":798},[137,90300,90301],{"class":364},"    \"main\"",[137,90303,726],{"class":157},[137,90305,90306],{"class":284},"\".\u002Fdist\u002Fmodule.mjs\"",[137,90308,1961],{"class":157},[137,90310,90311,90314,90316,90319],{"class":139,"line":803},[137,90312,90313],{"class":364},"    \"files\"",[137,90315,29669],{"class":157},[137,90317,90318],{"class":284},"\"dist\"",[137,90320,21916],{"class":157},[137,90322,90323,90325],{"class":139,"line":931},[137,90324,57350],{"class":364},[137,90326,1819],{"class":157},[137,90328,90329,90332,90334,90337],{"class":139,"line":1568},[137,90330,90331],{"class":364},"        \"prepack\"",[137,90333,726],{"class":157},[137,90335,90336],{"class":284},"\"nuxt-module-build build\"",[137,90338,1961],{"class":157},[137,90340,90341,90343,90345,90348],{"class":139,"line":1573},[137,90342,82828],{"class":364},[137,90344,726],{"class":157},[137,90346,90347],{"class":284},"\"npx nuxi dev playground\"",[137,90349,1961],{"class":157},[137,90351,90352,90355,90357],{"class":139,"line":1578},[137,90353,90354],{"class":364},"        \"dev:prepare\"",[137,90356,726],{"class":157},[137,90358,90359],{"class":284},"\"nuxt-module-build build --stub && nuxt-module-build prepare && npx nuxi prepare playground\"\n",[137,90361,90362],{"class":139,"line":1588},[137,90363,775],{"class":157},[137,90365,90366,90368],{"class":139,"line":1601},[137,90367,79830],{"class":364},[137,90369,1819],{"class":157},[137,90371,90372,90375,90377,90380],{"class":139,"line":3802},[137,90373,90374],{"class":364},"        \"@mcp-b\u002Fglobal\"",[137,90376,726],{"class":157},[137,90378,90379],{"class":284},"\"^2.0.13\"",[137,90381,1961],{"class":157},[137,90383,90384,90387,90389],{"class":139,"line":3808},[137,90385,90386],{"class":364},"        \"@nuxt\u002Fkit\"",[137,90388,726],{"class":157},[137,90390,90391],{"class":284},"\"^4.3.1\"\n",[137,90393,90394],{"class":139,"line":3822},[137,90395,775],{"class":157},[137,90397,90398,90400],{"class":139,"line":3827},[137,90399,80735],{"class":364},[137,90401,1819],{"class":157},[137,90403,90404,90407,90409,90412],{"class":139,"line":3832},[137,90405,90406],{"class":364},"        \"@nuxt\u002Fmodule-builder\"",[137,90408,726],{"class":157},[137,90410,90411],{"class":284},"\"^1.0.2\"",[137,90413,1961],{"class":157},[137,90415,90416,90419,90421,90424],{"class":139,"line":3840},[137,90417,90418],{"class":364},"        \"@nuxt\u002Fschema\"",[137,90420,726],{"class":157},[137,90422,90423],{"class":284},"\"^4.3.1\"",[137,90425,1961],{"class":157},[137,90427,90428,90431,90433,90435],{"class":139,"line":3846},[137,90429,90430],{"class":364},"        \"nuxt\"",[137,90432,726],{"class":157},[137,90434,90423],{"class":284},[137,90436,1961],{"class":157},[137,90438,90439,90442,90444],{"class":139,"line":3861},[137,90440,90441],{"class":364},"        \"typescript\"",[137,90443,726],{"class":157},[137,90445,90446],{"class":284},"\"^5.9.3\"\n",[137,90448,90449],{"class":139,"line":3883},[137,90450,294],{"class":157},[137,90452,90453],{"class":139,"line":3896},[137,90454,510],{"class":157},[27,90456,90457],{},"A few things to note here:",[1003,90459,90460,90483,90498,90509,90520,90528],{},[1006,90461,90462,90470,90471,164,90474,164,90477,14528,90480],{},[45,90463,90466],{"href":90464,"target":2716,"rel":90465},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fkit",[2718,2719],[42,90467,90468],{},[22,90469,90196],{}," is a runtime dependency. It provides the utilities your module uses: ",[22,90472,90473],{},"defineNuxtModule",[22,90475,90476],{},"addPlugin",[22,90478,90479],{},"addImports",[22,90481,90482],{},"createResolver",[1006,90484,90485,90493,90494,90497],{},[45,90486,90489],{"href":90487,"target":2716,"rel":90488},"https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fmodule-builder",[2718,2719],[42,90490,90491],{},[22,90492,90199],{}," is a dev dependency. It compiles your TypeScript source into the ",[22,90495,90496],{},"dist\u002F"," directory",[1006,90499,4737,90500,90504,90505,90508],{},[42,90501,90502],{},[22,90503,8756],{}," field points to ",[22,90506,90507],{},".\u002Fdist\u002Fmodule.mjs"," - this is the compiled entry point that Nuxt loads when someone installs your module",[1006,90510,4737,90511,90515,90516,90519],{},[42,90512,90513],{},[22,90514,90192],{}," script runs ",[22,90517,90518],{},"nuxt-module-build build",", which compiles everything before publishing",[1006,90521,4737,90522,90527],{},[42,90523,90524],{},[22,90525,90526],{},"dev"," script starts a playground app so you can test your module during development",[1006,90529,4737,90530,90535],{},[42,90531,90532],{},[22,90533,90534],{},"dev:prepare"," script stubs the module for development (so changes reflect immediately without rebuilding)",[27,90537,90538,90539,90542],{},"Run ",[22,90540,90541],{},"npm install"," to install everything.",[104,90544,90546],{"id":90545},"step-2-the-module-definition","Step 2: The Module Definition",[27,90548,90549,90550,90553,90554,20514,90556,90558],{},"This is the heart of the module. The ",[22,90551,90552],{},"src\u002Fmodule.ts"," file uses ",[22,90555,90473],{},[22,90557,90196],{}," to declare what the module does when Nuxt starts up.",[128,90560,90562],{"className":13299,"code":90561,"language":13301,"meta":133,"style":133},"\u002F\u002F src\u002Fmodule.ts\nimport { addImports, addPlugin, createResolver, defineNuxtModule } from \"@nuxt\u002Fkit\";\n\nexport interface TransportServerOptions {\n    allowedOrigins?: string[];\n    channelId?: string;\n}\n\nexport interface TransportConfiguration {\n    tabServer?: Partial\u003CTransportServerOptions> | false;\n    iframeServer?: Partial\u003CTransportServerOptions> | false;\n}\n\nexport interface ModuleOptions {\n    autoInitialize?: boolean;\n    transport?: TransportConfiguration;\n    nativeModelContextBehavior?: \"preserve\" | \"patch\";\n    installTestingShim?: boolean | \"always\" | \"if-missing\";\n}\n\nexport default defineNuxtModule\u003CModuleOptions>({\n    meta: {\n        name: \"nuxt-mcp-b\",\n        configKey: \"mcpB\",\n        compatibility: {\n            nuxt: \">=3.0.0\",\n        },\n    },\n    defaults: {\n        autoInitialize: true,\n        nativeModelContextBehavior: \"preserve\",\n        installTestingShim: \"if-missing\",\n    },\n    setup(options, nuxt) {\n        const resolver = createResolver(import.meta.url);\n\n        \u002F\u002F Pass module options to runtime via public runtimeConfig\n        nuxt.options.runtimeConfig.public.mcpB = {\n            autoInitialize: options.autoInitialize,\n            transport: options.transport,\n            nativeModelContextBehavior: options.nativeModelContextBehavior,\n            installTestingShim: options.installTestingShim,\n        };\n\n        \u002F\u002F Register the client-side plugin\n        addPlugin({\n            src: resolver.resolve(\".\u002Fruntime\u002Fapp\u002Fplugins\u002Fmcp-b.client\"),\n            mode: \"client\",\n        });\n\n        \u002F\u002F Register composables for auto-import\n        addImports([\n            {\n                name: \"useMcpB\",\n                from: resolver.resolve(\".\u002Fruntime\u002Fapp\u002Fcomposables\u002FuseMcpB\"),\n            },\n            {\n                name: \"useMcpTool\",\n                from: resolver.resolve(\".\u002Fruntime\u002Fapp\u002Fcomposables\u002FuseMcpTool\"),\n            },\n        ]);\n    },\n});\n",[22,90563,90564,90569,90583,90587,90599,90610,90621,90625,90629,90640,90663,90684,90688,90692,90703,90714,90725,90742,90763,90767,90771,90787,90792,90801,90811,90816,90826,90830,90834,90839,90848,90858,90868,90872,90888,90911,90915,90920,90929,90934,90939,90944,90949,90953,90957,90962,90969,90983,90993,90997,91001,91006,91014,91018,91027,91041,91045,91049,91058,91071,91075,91080,91084],{"__ignoreMap":133},[137,90565,90566],{"class":139,"line":140},[137,90567,90568],{"class":308},"\u002F\u002F src\u002Fmodule.ts\n",[137,90570,90571,90573,90576,90578,90581],{"class":139,"line":173},[137,90572,10287],{"class":143},[137,90574,90575],{"class":157}," { addImports, addPlugin, createResolver, defineNuxtModule } ",[137,90577,10954],{"class":143},[137,90579,90580],{"class":284}," \"@nuxt\u002Fkit\"",[137,90582,3276],{"class":157},[137,90584,90585],{"class":139,"line":188},[137,90586,516],{"emptyLinePlaceholder":515},[137,90588,90589,90591,90594,90597],{"class":139,"line":269},[137,90590,13456],{"class":143},[137,90592,90593],{"class":143}," interface",[137,90595,90596],{"class":147}," TransportServerOptions",[137,90598,256],{"class":157},[137,90600,90601,90604,90606,90608],{"class":139,"line":278},[137,90602,90603],{"class":161},"    allowedOrigins",[137,90605,35824],{"class":143},[137,90607,13630],{"class":364},[137,90609,14968],{"class":157},[137,90611,90612,90615,90617,90619],{"class":139,"line":291},[137,90613,90614],{"class":161},"    channelId",[137,90616,35824],{"class":143},[137,90618,13630],{"class":364},[137,90620,3276],{"class":157},[137,90622,90623],{"class":139,"line":297},[137,90624,510],{"class":157},[137,90626,90627],{"class":139,"line":302},[137,90628,516],{"emptyLinePlaceholder":515},[137,90630,90631,90633,90635,90638],{"class":139,"line":662},[137,90632,13456],{"class":143},[137,90634,90593],{"class":143},[137,90636,90637],{"class":147}," TransportConfiguration",[137,90639,256],{"class":157},[137,90641,90642,90645,90647,90650,90652,90655,90657,90659,90661],{"class":139,"line":667},[137,90643,90644],{"class":161},"    tabServer",[137,90646,35824],{"class":143},[137,90648,90649],{"class":147}," Partial",[137,90651,4033],{"class":157},[137,90653,90654],{"class":147},"TransportServerOptions",[137,90656,14124],{"class":157},[137,90658,7684],{"class":143},[137,90660,3387],{"class":364},[137,90662,3276],{"class":157},[137,90664,90665,90668,90670,90672,90674,90676,90678,90680,90682],{"class":139,"line":786},[137,90666,90667],{"class":161},"    iframeServer",[137,90669,35824],{"class":143},[137,90671,90649],{"class":147},[137,90673,4033],{"class":157},[137,90675,90654],{"class":147},[137,90677,14124],{"class":157},[137,90679,7684],{"class":143},[137,90681,3387],{"class":364},[137,90683,3276],{"class":157},[137,90685,90686],{"class":139,"line":798},[137,90687,510],{"class":157},[137,90689,90690],{"class":139,"line":803},[137,90691,516],{"emptyLinePlaceholder":515},[137,90693,90694,90696,90698,90701],{"class":139,"line":931},[137,90695,13456],{"class":143},[137,90697,90593],{"class":143},[137,90699,90700],{"class":147}," ModuleOptions",[137,90702,256],{"class":157},[137,90704,90705,90708,90710,90712],{"class":139,"line":1568},[137,90706,90707],{"class":161},"    autoInitialize",[137,90709,35824],{"class":143},[137,90711,14110],{"class":364},[137,90713,3276],{"class":157},[137,90715,90716,90719,90721,90723],{"class":139,"line":1573},[137,90717,90718],{"class":161},"    transport",[137,90720,35824],{"class":143},[137,90722,90637],{"class":147},[137,90724,3276],{"class":157},[137,90726,90727,90730,90732,90735,90737,90740],{"class":139,"line":1578},[137,90728,90729],{"class":161},"    nativeModelContextBehavior",[137,90731,35824],{"class":143},[137,90733,90734],{"class":284}," \"preserve\"",[137,90736,14113],{"class":143},[137,90738,90739],{"class":284}," \"patch\"",[137,90741,3276],{"class":157},[137,90743,90744,90747,90749,90751,90753,90756,90758,90761],{"class":139,"line":1588},[137,90745,90746],{"class":161},"    installTestingShim",[137,90748,35824],{"class":143},[137,90750,14110],{"class":364},[137,90752,14113],{"class":143},[137,90754,90755],{"class":284}," \"always\"",[137,90757,14113],{"class":143},[137,90759,90760],{"class":284}," \"if-missing\"",[137,90762,3276],{"class":157},[137,90764,90765],{"class":139,"line":1601},[137,90766,510],{"class":157},[137,90768,90769],{"class":139,"line":3802},[137,90770,516],{"emptyLinePlaceholder":515},[137,90772,90773,90775,90777,90780,90782,90785],{"class":139,"line":3808},[137,90774,13456],{"class":143},[137,90776,21723],{"class":143},[137,90778,90779],{"class":147}," defineNuxtModule",[137,90781,4033],{"class":157},[137,90783,90784],{"class":147},"ModuleOptions",[137,90786,74888],{"class":157},[137,90788,90789],{"class":139,"line":3822},[137,90790,90791],{"class":157},"    meta: {\n",[137,90793,90794,90797,90799],{"class":139,"line":3827},[137,90795,90796],{"class":157},"        name: ",[137,90798,90217],{"class":284},[137,90800,1961],{"class":157},[137,90802,90803,90806,90809],{"class":139,"line":3832},[137,90804,90805],{"class":157},"        configKey: ",[137,90807,90808],{"class":284},"\"mcpB\"",[137,90810,1961],{"class":157},[137,90812,90813],{"class":139,"line":3840},[137,90814,90815],{"class":157},"        compatibility: {\n",[137,90817,90818,90821,90824],{"class":139,"line":3846},[137,90819,90820],{"class":157},"            nuxt: ",[137,90822,90823],{"class":284},"\">=3.0.0\"",[137,90825,1961],{"class":157},[137,90827,90828],{"class":139,"line":3861},[137,90829,2084],{"class":157},[137,90831,90832],{"class":139,"line":3883},[137,90833,775],{"class":157},[137,90835,90836],{"class":139,"line":3896},[137,90837,90838],{"class":157},"    defaults: {\n",[137,90840,90841,90844,90846],{"class":139,"line":3901},[137,90842,90843],{"class":157},"        autoInitialize: ",[137,90845,3097],{"class":364},[137,90847,1961],{"class":157},[137,90849,90850,90853,90856],{"class":139,"line":3906},[137,90851,90852],{"class":157},"        nativeModelContextBehavior: ",[137,90854,90855],{"class":284},"\"preserve\"",[137,90857,1961],{"class":157},[137,90859,90860,90863,90866],{"class":139,"line":3911},[137,90861,90862],{"class":157},"        installTestingShim: ",[137,90864,90865],{"class":284},"\"if-missing\"",[137,90867,1961],{"class":157},[137,90869,90870],{"class":139,"line":4666},[137,90871,775],{"class":157},[137,90873,90874,90876,90878,90881,90883,90886],{"class":139,"line":4672},[137,90875,15255],{"class":147},[137,90877,356],{"class":157},[137,90879,90880],{"class":161},"options",[137,90882,164],{"class":157},[137,90884,90885],{"class":161},"nuxt",[137,90887,170],{"class":157},[137,90889,90890,90892,90895,90897,90900,90902,90904,90906,90908],{"class":139,"line":4680},[137,90891,3008],{"class":143},[137,90893,90894],{"class":364}," resolver",[137,90896,151],{"class":143},[137,90898,90899],{"class":147}," createResolver",[137,90901,356],{"class":157},[137,90903,10287],{"class":143},[137,90905,1017],{"class":157},[137,90907,23508],{"class":364},[137,90909,90910],{"class":157},".url);\n",[137,90912,90913],{"class":139,"line":4711},[137,90914,516],{"emptyLinePlaceholder":515},[137,90916,90917],{"class":139,"line":4716},[137,90918,90919],{"class":308},"        \u002F\u002F Pass module options to runtime via public runtimeConfig\n",[137,90921,90922,90925,90927],{"class":139,"line":4721},[137,90923,90924],{"class":157},"        nuxt.options.runtimeConfig.public.mcpB ",[137,90926,253],{"class":143},[137,90928,256],{"class":157},[137,90930,90931],{"class":139,"line":4727},[137,90932,90933],{"class":157},"            autoInitialize: options.autoInitialize,\n",[137,90935,90936],{"class":139,"line":4732},[137,90937,90938],{"class":157},"            transport: options.transport,\n",[137,90940,90941],{"class":139,"line":5006},[137,90942,90943],{"class":157},"            nativeModelContextBehavior: options.nativeModelContextBehavior,\n",[137,90945,90946],{"class":139,"line":5014},[137,90947,90948],{"class":157},"            installTestingShim: options.installTestingShim,\n",[137,90950,90951],{"class":139,"line":14343},[137,90952,1507],{"class":157},[137,90954,90955],{"class":139,"line":24199},[137,90956,516],{"emptyLinePlaceholder":515},[137,90958,90959],{"class":139,"line":24773},[137,90960,90961],{"class":308},"        \u002F\u002F Register the client-side plugin\n",[137,90963,90964,90967],{"class":139,"line":24778},[137,90965,90966],{"class":147},"        addPlugin",[137,90968,3175],{"class":157},[137,90970,90971,90974,90976,90978,90981],{"class":139,"line":24783},[137,90972,90973],{"class":157},"            src: resolver.",[137,90975,48591],{"class":147},[137,90977,356],{"class":157},[137,90979,90980],{"class":284},"\".\u002Fruntime\u002Fapp\u002Fplugins\u002Fmcp-b.client\"",[137,90982,4517],{"class":157},[137,90984,90985,90988,90991],{"class":139,"line":24793},[137,90986,90987],{"class":157},"            mode: ",[137,90989,90990],{"class":284},"\"client\"",[137,90992,1961],{"class":157},[137,90994,90995],{"class":139,"line":24827},[137,90996,14079],{"class":157},[137,90998,90999],{"class":139,"line":24857},[137,91000,516],{"emptyLinePlaceholder":515},[137,91002,91003],{"class":139,"line":24862},[137,91004,91005],{"class":308},"        \u002F\u002F Register composables for auto-import\n",[137,91007,91008,91011],{"class":139,"line":24867},[137,91009,91010],{"class":147},"        addImports",[137,91012,91013],{"class":157},"([\n",[137,91015,91016],{"class":139,"line":24884},[137,91017,81549],{"class":157},[137,91019,91020,91022,91025],{"class":139,"line":24892},[137,91021,37314],{"class":157},[137,91023,91024],{"class":284},"\"useMcpB\"",[137,91026,1961],{"class":157},[137,91028,91029,91032,91034,91036,91039],{"class":139,"line":24902},[137,91030,91031],{"class":157},"                from: resolver.",[137,91033,48591],{"class":147},[137,91035,356],{"class":157},[137,91037,91038],{"class":284},"\".\u002Fruntime\u002Fapp\u002Fcomposables\u002FuseMcpB\"",[137,91040,4517],{"class":157},[137,91042,91043],{"class":139,"line":24925},[137,91044,14074],{"class":157},[137,91046,91047],{"class":139,"line":24933},[137,91048,81549],{"class":157},[137,91050,91051,91053,91056],{"class":139,"line":24941},[137,91052,37314],{"class":157},[137,91054,91055],{"class":284},"\"useMcpTool\"",[137,91057,1961],{"class":157},[137,91059,91060,91062,91064,91066,91069],{"class":139,"line":24952},[137,91061,91031],{"class":157},[137,91063,48591],{"class":147},[137,91065,356],{"class":157},[137,91067,91068],{"class":284},"\".\u002Fruntime\u002Fapp\u002Fcomposables\u002FuseMcpTool\"",[137,91070,4517],{"class":157},[137,91072,91073],{"class":139,"line":24960},[137,91074,14074],{"class":157},[137,91076,91077],{"class":139,"line":24982},[137,91078,91079],{"class":157},"        ]);\n",[137,91081,91082],{"class":139,"line":24989},[137,91083,775],{"class":157},[137,91085,91086],{"class":139,"line":24994},[137,91087,5422],{"class":157},[27,91089,91090],{},"That's the entire module definition. Let's break it down piece by piece.",[123,91092,4737,91094,91096],{"id":91093},"the-meta-object",[22,91095,23508],{}," Object",[128,91098,91100],{"className":13299,"code":91099,"language":13301,"meta":133,"style":133},"meta: {\n    name: \"nuxt-mcp-b\",\n    configKey: \"mcpB\",\n    compatibility: {\n        nuxt: \">=3.0.0\",\n    },\n},\n",[22,91101,91102,91108,91119,91130,91137,91148,91152],{"__ignoreMap":133},[137,91103,91104,91106],{"class":139,"line":140},[137,91105,23508],{"class":147},[137,91107,1819],{"class":157},[137,91109,91110,91113,91115,91117],{"class":139,"line":173},[137,91111,91112],{"class":147},"    name",[137,91114,726],{"class":157},[137,91116,90217],{"class":284},[137,91118,1961],{"class":157},[137,91120,91121,91124,91126,91128],{"class":139,"line":188},[137,91122,91123],{"class":147},"    configKey",[137,91125,726],{"class":157},[137,91127,90808],{"class":284},[137,91129,1961],{"class":157},[137,91131,91132,91135],{"class":139,"line":269},[137,91133,91134],{"class":147},"    compatibility",[137,91136,1819],{"class":157},[137,91138,91139,91142,91144,91146],{"class":139,"line":278},[137,91140,91141],{"class":147},"        nuxt",[137,91143,726],{"class":157},[137,91145,90823],{"class":284},[137,91147,1961],{"class":157},[137,91149,91150],{"class":139,"line":291},[137,91151,775],{"class":157},[137,91153,91154],{"class":139,"line":297},[137,91155,1971],{"class":157},[1003,91157,91158,91165],{},[1006,91159,91160,91164],{},[42,91161,91162],{},[22,91163,1387],{}," is the module's identifier. It shows up in logs and dev tools",[1006,91166,91167,91172,91173,91175,91176,91179],{},[42,91168,91169],{},[22,91170,91171],{},"configKey"," tells Nuxt which key in ",[22,91174,21711],{}," holds this module's options. With ",[22,91177,91178],{},"configKey: \"mcpB\"",", users configure the module like this:",[128,91181,91183],{"className":13299,"code":91182,"language":13301,"meta":133,"style":133},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n    modules: [\"nuxt-mcp-b\"],\n    mcpB: {\n        autoInitialize: true,\n    },\n});\n",[22,91184,91185,91190,91200,91209,91214,91222,91226],{"__ignoreMap":133},[137,91186,91187],{"class":139,"line":140},[137,91188,91189],{"class":308},"\u002F\u002F nuxt.config.ts\n",[137,91191,91192,91194,91196,91198],{"class":139,"line":173},[137,91193,13456],{"class":143},[137,91195,21723],{"class":143},[137,91197,21726],{"class":147},[137,91199,3175],{"class":157},[137,91201,91202,91205,91207],{"class":139,"line":188},[137,91203,91204],{"class":157},"    modules: [",[137,91206,90217],{"class":284},[137,91208,21916],{"class":157},[137,91210,91211],{"class":139,"line":269},[137,91212,91213],{"class":157},"    mcpB: {\n",[137,91215,91216,91218,91220],{"class":139,"line":278},[137,91217,90843],{"class":157},[137,91219,3097],{"class":364},[137,91221,1961],{"class":157},[137,91223,91224],{"class":139,"line":291},[137,91225,775],{"class":157},[137,91227,91228],{"class":139,"line":297},[137,91229,5422],{"class":157},[1003,91231,91232],{},[1006,91233,91234,91239],{},[42,91235,91236],{},[22,91237,91238],{},"compatibility"," specifies which versions of Nuxt your module supports",[123,91241,4737,91243,91096],{"id":91242},"the-defaults-object",[22,91244,91245],{},"defaults",[128,91247,91249],{"className":13299,"code":91248,"language":13301,"meta":133,"style":133},"defaults: {\n    autoInitialize: true,\n    nativeModelContextBehavior: \"preserve\",\n    installTestingShim: \"if-missing\",\n},\n",[22,91250,91251,91257,91267,91277,91287],{"__ignoreMap":133},[137,91252,91253,91255],{"class":139,"line":140},[137,91254,91245],{"class":147},[137,91256,1819],{"class":157},[137,91258,91259,91261,91263,91265],{"class":139,"line":173},[137,91260,90707],{"class":147},[137,91262,726],{"class":157},[137,91264,3097],{"class":364},[137,91266,1961],{"class":157},[137,91268,91269,91271,91273,91275],{"class":139,"line":188},[137,91270,90729],{"class":147},[137,91272,726],{"class":157},[137,91274,90855],{"class":284},[137,91276,1961],{"class":157},[137,91278,91279,91281,91283,91285],{"class":139,"line":269},[137,91280,90746],{"class":147},[137,91282,726],{"class":157},[137,91284,90865],{"class":284},[137,91286,1961],{"class":157},[137,91288,91289],{"class":139,"line":278},[137,91290,1971],{"class":157},[27,91292,91293,91294,91297,91298,91300,91301,1017],{},"These are the default values for your module options. Nuxt merges them with whatever the user provides in their config. If a user doesn't specify ",[22,91295,91296],{},"autoInitialize",", it defaults to ",[22,91299,3097],{},". This is important - ",[42,91302,91303],{},"good defaults mean most users get a working module without any configuration at all",[123,91305,4737,91307,35231],{"id":91306},"the-setup-function",[22,91308,15609],{},[27,91310,91311,91312,91314],{},"This is where the real work happens. The ",[22,91313,15609],{}," function receives two arguments:",[1003,91316,91317,91324],{},[1006,91318,91319,91323],{},[42,91320,91321],{},[22,91322,90880],{}," - the merged module options (your defaults + whatever the user configured)",[1006,91325,91326,91330],{},[42,91327,91328],{},[22,91329,90885],{}," - the Nuxt instance, which gives you access to the full Nuxt configuration",[27,91332,4370,91333,91335],{},[22,91334,15609],{},", we do three things. Let's look at each one.",[27,91337,91338],{},[42,91339,91340],{},"1. Pass options to runtime config",[128,91342,91344],{"className":13299,"code":91343,"language":13301,"meta":133,"style":133},"nuxt.options.runtimeConfig.public.mcpB = {\n    autoInitialize: options.autoInitialize,\n    transport: options.transport,\n    nativeModelContextBehavior: options.nativeModelContextBehavior,\n    installTestingShim: options.installTestingShim,\n};\n",[22,91345,91346,91355,91360,91365,91370,91375],{"__ignoreMap":133},[137,91347,91348,91351,91353],{"class":139,"line":140},[137,91349,91350],{"class":157},"nuxt.options.runtimeConfig.public.mcpB ",[137,91352,253],{"class":143},[137,91354,256],{"class":157},[137,91356,91357],{"class":139,"line":173},[137,91358,91359],{"class":157},"    autoInitialize: options.autoInitialize,\n",[137,91361,91362],{"class":139,"line":188},[137,91363,91364],{"class":157},"    transport: options.transport,\n",[137,91366,91367],{"class":139,"line":269},[137,91368,91369],{"class":157},"    nativeModelContextBehavior: options.nativeModelContextBehavior,\n",[137,91371,91372],{"class":139,"line":278},[137,91373,91374],{"class":157},"    installTestingShim: options.installTestingShim,\n",[137,91376,91377],{"class":139,"line":291},[137,91378,191],{"class":157},[27,91380,91381,91382,91384,91385,91392],{},"Remember the two worlds? The ",[22,91383,15609],{}," function runs at build time, but our plugin needs these options at application time. ",[45,91386,91389],{"href":91387,"target":2716,"rel":91388},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Fgoing-further\u002Fruntime-config#exposing-runtime-config",[2718,2719],[22,91390,91391],{},"runtimeConfig.public"," is the bridge - it serialises the values into the page so they're available in both server and client contexts.",[27,91394,91395],{},[42,91396,91397],{},"2. Register a plugin",[128,91399,91401],{"className":13299,"code":91400,"language":13301,"meta":133,"style":133},"addPlugin({\n    src: resolver.resolve(\".\u002Fruntime\u002Fapp\u002Fplugins\u002Fmcp-b.client\"),\n    mode: \"client\",\n});\n",[22,91402,91403,91409,91422,91431],{"__ignoreMap":133},[137,91404,91405,91407],{"class":139,"line":140},[137,91406,90476],{"class":147},[137,91408,3175],{"class":157},[137,91410,91411,91414,91416,91418,91420],{"class":139,"line":173},[137,91412,91413],{"class":157},"    src: resolver.",[137,91415,48591],{"class":147},[137,91417,356],{"class":157},[137,91419,90980],{"class":284},[137,91421,4517],{"class":157},[137,91423,91424,91427,91429],{"class":139,"line":188},[137,91425,91426],{"class":157},"    mode: ",[137,91428,90990],{"class":284},[137,91430,1961],{"class":157},[137,91432,91433],{"class":139,"line":269},[137,91434,5422],{"class":157},[27,91436,91437,91439,91440,91443,91444,91446,91447,114,91450,91453],{},[22,91438,90476],{}," tells Nuxt to include our plugin file in the application. The ",[22,91441,91442],{},"mode: \"client\""," flag is critical - it ensures the plugin only runs in the browser, not during server-side rendering. Since ",[22,91445,89099],{}," needs ",[22,91448,91449],{},"window",[22,91451,91452],{},"navigator",", running it on the server would crash.",[27,91455,91456],{},[42,91457,91458],{},"3. Register composables",[128,91460,91462],{"className":13299,"code":91461,"language":13301,"meta":133,"style":133},"addImports([\n    {\n        name: \"useMcpB\",\n        from: resolver.resolve(\".\u002Fruntime\u002Fapp\u002Fcomposables\u002FuseMcpB\"),\n    },\n    {\n        name: \"useMcpTool\",\n        from: resolver.resolve(\".\u002Fruntime\u002Fapp\u002Fcomposables\u002FuseMcpTool\"),\n    },\n]);\n",[22,91463,91464,91470,91474,91482,91495,91499,91503,91511,91523,91527],{"__ignoreMap":133},[137,91465,91466,91468],{"class":139,"line":140},[137,91467,90479],{"class":147},[137,91469,91013],{"class":157},[137,91471,91472],{"class":139,"line":173},[137,91473,28051],{"class":157},[137,91475,91476,91478,91480],{"class":139,"line":188},[137,91477,90796],{"class":157},[137,91479,91024],{"class":284},[137,91481,1961],{"class":157},[137,91483,91484,91487,91489,91491,91493],{"class":139,"line":269},[137,91485,91486],{"class":157},"        from: resolver.",[137,91488,48591],{"class":147},[137,91490,356],{"class":157},[137,91492,91038],{"class":284},[137,91494,4517],{"class":157},[137,91496,91497],{"class":139,"line":278},[137,91498,775],{"class":157},[137,91500,91501],{"class":139,"line":291},[137,91502,28051],{"class":157},[137,91504,91505,91507,91509],{"class":139,"line":297},[137,91506,90796],{"class":157},[137,91508,91055],{"class":284},[137,91510,1961],{"class":157},[137,91512,91513,91515,91517,91519,91521],{"class":139,"line":302},[137,91514,91486],{"class":157},[137,91516,48591],{"class":147},[137,91518,356],{"class":157},[137,91520,91068],{"class":284},[137,91522,4517],{"class":157},[137,91524,91525],{"class":139,"line":662},[137,91526,775],{"class":157},[137,91528,91529],{"class":139,"line":667},[137,91530,43556],{"class":157},[27,91532,91533,91535,91536,91539,91540,91543],{},[22,91534,90479],{}," makes our composables available as ",[45,91537,89978],{"href":89976,"target":2716,"rel":91538},[2718,2719]," throughout the user's application. After this, users can write ",[22,91541,91542],{},"useMcpTool(...)"," in any component without importing anything manually. This is one of the things that makes Nuxt modules feel so seamless.",[123,91545,56439,91547,91549],{"id":91546},"why-createresolver-matters",[22,91548,90482],{}," Matters",[128,91551,91553],{"className":13299,"code":91552,"language":13301,"meta":133,"style":133},"const resolver = createResolver(import.meta.url);\nresolver.resolve(\".\u002Fruntime\u002Fapp\u002Fplugins\u002Fmcp-b.client\");\n",[22,91554,91555,91575],{"__ignoreMap":133},[137,91556,91557,91559,91561,91563,91565,91567,91569,91571,91573],{"class":139,"line":140},[137,91558,3077],{"class":143},[137,91560,90894],{"class":364},[137,91562,151],{"class":143},[137,91564,90899],{"class":147},[137,91566,356],{"class":157},[137,91568,10287],{"class":143},[137,91570,1017],{"class":157},[137,91572,23508],{"class":364},[137,91574,90910],{"class":157},[137,91576,91577,91580,91582,91584,91586],{"class":139,"line":173},[137,91578,91579],{"class":157},"resolver.",[137,91581,48591],{"class":147},[137,91583,356],{"class":157},[137,91585,90980],{"class":284},[137,91587,1502],{"class":157},[27,91589,91590,91591,12972],{},"Here's something that confused me at first. Why can't we just use a regular relative path like ",[22,91592,90980],{},[27,91594,91595,91596,91599,91600,91602],{},"The problem is that when your module is installed in someone else's project, it lives deep inside ",[22,91597,91598],{},"node_modules\u002F",". A plain relative path would resolve relative to the user's project root - not your module's directory. ",[22,91601,90482],{}," creates a path resolver anchored to your module file's location, so paths always resolve correctly regardless of where the module is installed.",[27,91604,91605],{},"This is a small detail, but it will save you hours of debugging if you remember it from the start.",[104,91607,91609],{"id":91608},"step-3-the-client-plugin","Step 3: The Client Plugin",[27,91611,91612,91613,91615],{},"The plugin is the runtime code that actually initialises ",[22,91614,89099],{}," when the page loads in the browser.",[128,91617,91619],{"className":13299,"code":91618,"language":13301,"meta":133,"style":133},"\u002F\u002F src\u002Fruntime\u002Fapp\u002Fplugins\u002Fmcp-b.client.ts\nimport { initializeWebModelContext } from \"@mcp-b\u002Fglobal\";\nimport { defineNuxtPlugin, useRuntimeConfig } from \"#imports\";\n\nexport default defineNuxtPlugin(() => {\n    const config = useRuntimeConfig();\n    const options = config.public.mcpB as {\n        autoInitialize?: boolean;\n        transport?: Record\u003Cstring, unknown>;\n        nativeModelContextBehavior?: \"preserve\" | \"patch\";\n        installTestingShim?: boolean | \"always\" | \"if-missing\";\n    };\n\n    if (options?.autoInitialize === false) {\n        return;\n    }\n\n    initializeWebModelContext({\n        transport: options?.transport as Parameters\u003Ctypeof initializeWebModelContext>[0][\"transport\"],\n        nativeModelContextBehavior: options?.nativeModelContextBehavior,\n        installTestingShim: options?.installTestingShim,\n    });\n});\n",[22,91620,91621,91626,91639,91653,91657,91671,91685,91701,91712,91732,91747,91766,91770,91774,91787,91793,91797,91801,91808,91835,91840,91845,91849],{"__ignoreMap":133},[137,91622,91623],{"class":139,"line":140},[137,91624,91625],{"class":308},"\u002F\u002F src\u002Fruntime\u002Fapp\u002Fplugins\u002Fmcp-b.client.ts\n",[137,91627,91628,91630,91633,91635,91637],{"class":139,"line":173},[137,91629,10287],{"class":143},[137,91631,91632],{"class":157}," { initializeWebModelContext } ",[137,91634,10954],{"class":143},[137,91636,89219],{"class":284},[137,91638,3276],{"class":157},[137,91640,91641,91643,91646,91648,91651],{"class":139,"line":188},[137,91642,10287],{"class":143},[137,91644,91645],{"class":157}," { defineNuxtPlugin, useRuntimeConfig } ",[137,91647,10954],{"class":143},[137,91649,91650],{"class":284}," \"#imports\"",[137,91652,3276],{"class":157},[137,91654,91655],{"class":139,"line":269},[137,91656,516],{"emptyLinePlaceholder":515},[137,91658,91659,91661,91663,91665,91667,91669],{"class":139,"line":278},[137,91660,13456],{"class":143},[137,91662,21723],{"class":143},[137,91664,33659],{"class":147},[137,91666,3193],{"class":157},[137,91668,222],{"class":143},[137,91670,256],{"class":157},[137,91672,91673,91675,91678,91680,91683],{"class":139,"line":291},[137,91674,4177],{"class":143},[137,91676,91677],{"class":364}," config",[137,91679,151],{"class":143},[137,91681,91682],{"class":147}," useRuntimeConfig",[137,91684,924],{"class":157},[137,91686,91687,91689,91692,91694,91697,91699],{"class":139,"line":297},[137,91688,4177],{"class":143},[137,91690,91691],{"class":364}," options",[137,91693,151],{"class":143},[137,91695,91696],{"class":157}," config.public.mcpB ",[137,91698,24431],{"class":143},[137,91700,256],{"class":157},[137,91702,91703,91706,91708,91710],{"class":139,"line":302},[137,91704,91705],{"class":161},"        autoInitialize",[137,91707,35824],{"class":143},[137,91709,14110],{"class":364},[137,91711,3276],{"class":157},[137,91713,91714,91717,91719,91721,91723,91725,91727,91730],{"class":139,"line":662},[137,91715,91716],{"class":161},"        transport",[137,91718,35824],{"class":143},[137,91720,72829],{"class":147},[137,91722,4033],{"class":157},[137,91724,14158],{"class":364},[137,91726,164],{"class":157},[137,91728,91729],{"class":364},"unknown",[137,91731,29320],{"class":157},[137,91733,91734,91737,91739,91741,91743,91745],{"class":139,"line":667},[137,91735,91736],{"class":161},"        nativeModelContextBehavior",[137,91738,35824],{"class":143},[137,91740,90734],{"class":284},[137,91742,14113],{"class":143},[137,91744,90739],{"class":284},[137,91746,3276],{"class":157},[137,91748,91749,91752,91754,91756,91758,91760,91762,91764],{"class":139,"line":786},[137,91750,91751],{"class":161},"        installTestingShim",[137,91753,35824],{"class":143},[137,91755,14110],{"class":364},[137,91757,14113],{"class":143},[137,91759,90755],{"class":284},[137,91761,14113],{"class":143},[137,91763,90760],{"class":284},[137,91765,3276],{"class":157},[137,91767,91768],{"class":139,"line":798},[137,91769,1892],{"class":157},[137,91771,91772],{"class":139,"line":803},[137,91773,516],{"emptyLinePlaceholder":515},[137,91775,91776,91778,91781,91783,91785],{"class":139,"line":931},[137,91777,24696],{"class":143},[137,91779,91780],{"class":157}," (options?.autoInitialize ",[137,91782,5502],{"class":143},[137,91784,3387],{"class":364},[137,91786,170],{"class":157},[137,91788,91789,91791],{"class":139,"line":1568},[137,91790,5472],{"class":143},[137,91792,3276],{"class":157},[137,91794,91795],{"class":139,"line":1573},[137,91796,294],{"class":157},[137,91798,91799],{"class":139,"line":1578},[137,91800,516],{"emptyLinePlaceholder":515},[137,91802,91803,91806],{"class":139,"line":1588},[137,91804,91805],{"class":147},"    initializeWebModelContext",[137,91807,3175],{"class":157},[137,91809,91810,91813,91815,91818,91820,91822,91825,91827,91830,91833],{"class":139,"line":1601},[137,91811,91812],{"class":157},"        transport: options?.transport ",[137,91814,24431],{"class":143},[137,91816,91817],{"class":147}," Parameters",[137,91819,4033],{"class":157},[137,91821,66007],{"class":143},[137,91823,91824],{"class":157}," initializeWebModelContext>[",[137,91826,6044],{"class":364},[137,91828,91829],{"class":157},"][",[137,91831,91832],{"class":284},"\"transport\"",[137,91834,21916],{"class":157},[137,91836,91837],{"class":139,"line":3802},[137,91838,91839],{"class":157},"        nativeModelContextBehavior: options?.nativeModelContextBehavior,\n",[137,91841,91842],{"class":139,"line":3808},[137,91843,91844],{"class":157},"        installTestingShim: options?.installTestingShim,\n",[137,91846,91847],{"class":139,"line":3822},[137,91848,2832],{"class":157},[137,91850,91851],{"class":139,"line":3827},[137,91852,5422],{"class":157},[27,91854,91855],{},"A few things worth noting here:",[27,91857,91858,91864,91865,91870,91871,91874,91875,91877,91878,91880],{},[42,91859,91860,91861,1017],{},"The file name ends with ",[22,91862,91863],{},".client.ts"," This is a ",[45,91866,91869],{"href":91867,"target":2716,"rel":91868},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Fdirectory-structure\u002Fplugins#plugin-registration-order",[2718,2719],"Nuxt convention"," - files with ",[22,91872,91873],{},".client"," in the name only run in the browser. Combined with the ",[22,91876,91442],{}," we set in ",[22,91879,90476],{},", we get double protection against SSR execution. I like having both because it makes the intent explicit.",[27,91882,91883,91888,91889,91891],{},[42,91884,91885,91886,1017],{},"We read options from ",[22,91887,90134],{}," This is the other end of the bridge we set up in ",[22,91890,90102],{},". The options were serialised into the page at build time, and now we read them back at application time.",[27,91893,91894,91899,91900,91903,91904,91906],{},[42,91895,91896,91897,1017],{},"We check ",[22,91898,91296],{}," If the user has set ",[22,91901,91902],{},"autoInitialize: false"," in their config, the plugin does nothing. This leaves manual initialisation to the user via the ",[22,91905,90026],{}," composable - a flexibility that some users need.",[27,91908,91909,91919,91920,114,91926,91933,91934,91936,91937,1017],{},[42,91910,91911,91912,1017],{},"We import from ",[45,91913,91916],{"href":91914,"target":2716,"rel":91915},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Fconcepts\u002Fauto-imports#auto-imported-components",[2718,2719],[22,91917,91918],{},"#imports"," This is a Nuxt-specific import alias that gives you access to framework utilities like ",[45,91921,91924],{"href":91922,"target":2716,"rel":91923},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Fdirectory-structure\u002Fplugins#creating-plugins",[2718,2719],[22,91925,33803],{},[45,91927,91930],{"href":91928,"target":2716,"rel":91929},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fcomposables\u002Fuse-runtime-config",[2718,2719],[22,91931,91932],{},"useRuntimeConfig",". It only works inside ",[22,91935,90121],{}," files - not in your ",[22,91938,90102],{},[104,91940,91942],{"id":91941},"step-4-the-composables","Step 4: The Composables",[27,91944,91945,91946,91949],{},"Composables are reusable functions that follow Vue's ",[45,91947,9597],{"href":12417,"target":2716,"rel":91948},[2718,2719]," patterns. We provide two of them, each serving a different use case.",[123,91951,91953,91955],{"id":91952},"usemcptool-register-tools-with-lifecycle-management",[22,91954,90023],{}," - Register Tools with Lifecycle Management",[27,91957,91958],{},"This is the composable most users will reach for. It registers an MCP tool when a component mounts and automatically unregisters it when the component unmounts. No cleanup code required.",[128,91960,91962],{"className":13299,"code":91961,"language":13301,"meta":133,"style":133},"\u002F\u002F src\u002Fruntime\u002Fapp\u002Fcomposables\u002FuseMcpTool.ts\nimport { onMounted, onUnmounted } from \"vue\";\n\ninterface ToolContent {\n    type: string;\n    text: string;\n}\n\ninterface ToolResponse {\n    content?: ToolContent[];\n    isError?: boolean;\n    [key: string]: unknown;\n}\n\ninterface ToolInputSchema {\n    type: \"object\";\n    properties: Record\u003Cstring, unknown>;\n    required?: string[];\n}\n\ninterface McpToolOptions {\n    name: string;\n    description: string;\n    inputSchema: ToolInputSchema;\n    execute: (args: Record\u003Cstring, unknown>) => Promise\u003CToolResponse>;\n}\n\nexport function useMcpTool(options: McpToolOptions): void {\n    let unregister: (() => void) | null = null;\n\n    onMounted(() => {\n        if (typeof navigator === \"undefined\" || !navigator.modelContext) {\n            return;\n        }\n\n        const registration = navigator.modelContext.registerTool({\n            name: options.name,\n            description: options.description,\n            inputSchema: options.inputSchema,\n            execute: options.execute,\n        });\n\n        unregister = () => registration.unregister();\n    });\n\n    onUnmounted(() => {\n        unregister?.();\n        unregister = null;\n    });\n}\n",[22,91963,91964,91969,91981,91985,91994,92004,92015,92019,92023,92032,92042,92053,92072,92076,92080,92089,92100,92119,92130,92134,92138,92147,92157,92168,92179,92215,92219,92223,92248,92276,92280,92290,92312,92318,92322,92326,92342,92347,92352,92357,92362,92366,92370,92389,92393,92397,92407,92414,92425,92429],{"__ignoreMap":133},[137,91965,91966],{"class":139,"line":140},[137,91967,91968],{"class":308},"\u002F\u002F src\u002Fruntime\u002Fapp\u002Fcomposables\u002FuseMcpTool.ts\n",[137,91970,91971,91973,91975,91977,91979],{"class":139,"line":173},[137,91972,10287],{"class":143},[137,91974,42040],{"class":157},[137,91976,10954],{"class":143},[137,91978,11091],{"class":284},[137,91980,3276],{"class":157},[137,91982,91983],{"class":139,"line":188},[137,91984,516],{"emptyLinePlaceholder":515},[137,91986,91987,91989,91992],{"class":139,"line":269},[137,91988,48479],{"class":143},[137,91990,91991],{"class":147}," ToolContent",[137,91993,256],{"class":157},[137,91995,91996,91998,92000,92002],{"class":139,"line":278},[137,91997,50901],{"class":161},[137,91999,894],{"class":143},[137,92001,13630],{"class":364},[137,92003,3276],{"class":157},[137,92005,92006,92009,92011,92013],{"class":139,"line":291},[137,92007,92008],{"class":161},"    text",[137,92010,894],{"class":143},[137,92012,13630],{"class":364},[137,92014,3276],{"class":157},[137,92016,92017],{"class":139,"line":297},[137,92018,510],{"class":157},[137,92020,92021],{"class":139,"line":302},[137,92022,516],{"emptyLinePlaceholder":515},[137,92024,92025,92027,92030],{"class":139,"line":662},[137,92026,48479],{"class":143},[137,92028,92029],{"class":147}," ToolResponse",[137,92031,256],{"class":157},[137,92033,92034,92036,92038,92040],{"class":139,"line":667},[137,92035,48521],{"class":161},[137,92037,35824],{"class":143},[137,92039,91991],{"class":147},[137,92041,14968],{"class":157},[137,92043,92044,92047,92049,92051],{"class":139,"line":786},[137,92045,92046],{"class":161},"    isError",[137,92048,35824],{"class":143},[137,92050,14110],{"class":364},[137,92052,3276],{"class":157},[137,92054,92055,92058,92060,92062,92064,92066,92068,92070],{"class":139,"line":798},[137,92056,92057],{"class":157},"    [",[137,92059,12632],{"class":161},[137,92061,894],{"class":143},[137,92063,13630],{"class":364},[137,92065,35111],{"class":157},[137,92067,894],{"class":143},[137,92069,74694],{"class":364},[137,92071,3276],{"class":157},[137,92073,92074],{"class":139,"line":803},[137,92075,510],{"class":157},[137,92077,92078],{"class":139,"line":931},[137,92079,516],{"emptyLinePlaceholder":515},[137,92081,92082,92084,92087],{"class":139,"line":1568},[137,92083,48479],{"class":143},[137,92085,92086],{"class":147}," ToolInputSchema",[137,92088,256],{"class":157},[137,92090,92091,92093,92095,92098],{"class":139,"line":1573},[137,92092,50901],{"class":161},[137,92094,894],{"class":143},[137,92096,92097],{"class":284}," \"object\"",[137,92099,3276],{"class":157},[137,92101,92102,92105,92107,92109,92111,92113,92115,92117],{"class":139,"line":1578},[137,92103,92104],{"class":161},"    properties",[137,92106,894],{"class":143},[137,92108,72829],{"class":147},[137,92110,4033],{"class":157},[137,92112,14158],{"class":364},[137,92114,164],{"class":157},[137,92116,91729],{"class":364},[137,92118,29320],{"class":157},[137,92120,92121,92124,92126,92128],{"class":139,"line":1588},[137,92122,92123],{"class":161},"    required",[137,92125,35824],{"class":143},[137,92127,13630],{"class":364},[137,92129,14968],{"class":157},[137,92131,92132],{"class":139,"line":1601},[137,92133,510],{"class":157},[137,92135,92136],{"class":139,"line":3802},[137,92137,516],{"emptyLinePlaceholder":515},[137,92139,92140,92142,92145],{"class":139,"line":3808},[137,92141,48479],{"class":143},[137,92143,92144],{"class":147}," McpToolOptions",[137,92146,256],{"class":157},[137,92148,92149,92151,92153,92155],{"class":139,"line":3822},[137,92150,91112],{"class":161},[137,92152,894],{"class":143},[137,92154,13630],{"class":364},[137,92156,3276],{"class":157},[137,92158,92159,92162,92164,92166],{"class":139,"line":3827},[137,92160,92161],{"class":161},"    description",[137,92163,894],{"class":143},[137,92165,13630],{"class":364},[137,92167,3276],{"class":157},[137,92169,92170,92173,92175,92177],{"class":139,"line":3832},[137,92171,92172],{"class":161},"    inputSchema",[137,92174,894],{"class":143},[137,92176,92086],{"class":147},[137,92178,3276],{"class":157},[137,92180,92181,92183,92185,92187,92189,92191,92193,92195,92197,92199,92201,92204,92206,92208,92210,92213],{"class":139,"line":3840},[137,92182,88413],{"class":147},[137,92184,894],{"class":143},[137,92186,158],{"class":157},[137,92188,72840],{"class":161},[137,92190,894],{"class":143},[137,92192,72829],{"class":147},[137,92194,4033],{"class":157},[137,92196,14158],{"class":364},[137,92198,164],{"class":157},[137,92200,91729],{"class":364},[137,92202,92203],{"class":157},">) ",[137,92205,222],{"class":143},[137,92207,14116],{"class":147},[137,92209,4033],{"class":157},[137,92211,92212],{"class":147},"ToolResponse",[137,92214,29320],{"class":157},[137,92216,92217],{"class":139,"line":3846},[137,92218,510],{"class":157},[137,92220,92221],{"class":139,"line":3861},[137,92222,516],{"emptyLinePlaceholder":515},[137,92224,92225,92227,92229,92232,92234,92236,92238,92240,92242,92244,92246],{"class":139,"line":3883},[137,92226,13456],{"class":143},[137,92228,154],{"class":143},[137,92230,92231],{"class":147}," useMcpTool",[137,92233,356],{"class":157},[137,92235,90880],{"class":161},[137,92237,894],{"class":143},[137,92239,92144],{"class":147},[137,92241,14105],{"class":157},[137,92243,894],{"class":143},[137,92245,35793],{"class":364},[137,92247,256],{"class":157},[137,92249,92250,92252,92255,92257,92260,92262,92264,92266,92268,92270,92272,92274],{"class":139,"line":3896},[137,92251,58054],{"class":143},[137,92253,92254],{"class":157}," unregister",[137,92256,894],{"class":143},[137,92258,92259],{"class":157}," (() ",[137,92261,222],{"class":143},[137,92263,35793],{"class":364},[137,92265,219],{"class":157},[137,92267,7684],{"class":143},[137,92269,3417],{"class":364},[137,92271,151],{"class":143},[137,92273,3417],{"class":364},[137,92275,3276],{"class":157},[137,92277,92278],{"class":139,"line":3901},[137,92279,516],{"emptyLinePlaceholder":515},[137,92281,92282,92284,92286,92288],{"class":139,"line":3906},[137,92283,34654],{"class":147},[137,92285,3193],{"class":157},[137,92287,222],{"class":143},[137,92289,256],{"class":157},[137,92291,92292,92294,92296,92298,92301,92303,92305,92307,92309],{"class":139,"line":3911},[137,92293,5496],{"class":143},[137,92295,158],{"class":157},[137,92297,66007],{"class":143},[137,92299,92300],{"class":157}," navigator ",[137,92302,5502],{"class":143},[137,92304,64387],{"class":284},[137,92306,24707],{"class":143},[137,92308,27133],{"class":143},[137,92310,92311],{"class":157},"navigator.modelContext) {\n",[137,92313,92314,92316],{"class":139,"line":4666},[137,92315,4683],{"class":143},[137,92317,3276],{"class":157},[137,92319,92320],{"class":139,"line":4672},[137,92321,1966],{"class":157},[137,92323,92324],{"class":139,"line":4680},[137,92325,516],{"emptyLinePlaceholder":515},[137,92327,92328,92330,92333,92335,92338,92340],{"class":139,"line":4711},[137,92329,3008],{"class":143},[137,92331,92332],{"class":364}," registration",[137,92334,151],{"class":143},[137,92336,92337],{"class":157}," navigator.modelContext.",[137,92339,88280],{"class":147},[137,92341,3175],{"class":157},[137,92343,92344],{"class":139,"line":4716},[137,92345,92346],{"class":157},"            name: options.name,\n",[137,92348,92349],{"class":139,"line":4721},[137,92350,92351],{"class":157},"            description: options.description,\n",[137,92353,92354],{"class":139,"line":4727},[137,92355,92356],{"class":157},"            inputSchema: options.inputSchema,\n",[137,92358,92359],{"class":139,"line":4732},[137,92360,92361],{"class":157},"            execute: options.execute,\n",[137,92363,92364],{"class":139,"line":5006},[137,92365,14079],{"class":157},[137,92367,92368],{"class":139,"line":5014},[137,92369,516],{"emptyLinePlaceholder":515},[137,92371,92372,92375,92377,92379,92381,92384,92387],{"class":139,"line":14343},[137,92373,92374],{"class":147},"        unregister",[137,92376,151],{"class":143},[137,92378,1484],{"class":157},[137,92380,222],{"class":143},[137,92382,92383],{"class":157}," registration.",[137,92385,92386],{"class":147},"unregister",[137,92388,924],{"class":157},[137,92390,92391],{"class":139,"line":24199},[137,92392,2832],{"class":157},[137,92394,92395],{"class":139,"line":24773},[137,92396,516],{"emptyLinePlaceholder":515},[137,92398,92399,92401,92403,92405],{"class":139,"line":24778},[137,92400,42085],{"class":147},[137,92402,3193],{"class":157},[137,92404,222],{"class":143},[137,92406,256],{"class":157},[137,92408,92409,92411],{"class":139,"line":24783},[137,92410,92374],{"class":147},[137,92412,92413],{"class":157},"?.();\n",[137,92415,92416,92419,92421,92423],{"class":139,"line":24793},[137,92417,92418],{"class":157},"        unregister ",[137,92420,253],{"class":143},[137,92422,3417],{"class":364},[137,92424,3276],{"class":157},[137,92426,92427],{"class":139,"line":24827},[137,92428,2832],{"class":157},[137,92430,92431],{"class":139,"line":24857},[137,92432,510],{"class":157},[27,92434,92435],{},"Let's walk through what's happening here.",[27,92437,4737,92438,92441,92442,92445,92446,164,92448,164,92450,92452,92453,92455],{},[42,92439,92440],{},"interfaces at the top"," define the shape of the tool options. ",[22,92443,92444],{},"McpToolOptions"," is the main one - it requires a ",[22,92447,1387],{},[22,92449,88555],{},[22,92451,88563],{}," (a JSON Schema for the tool's input parameters), and an ",[22,92454,88571],{}," function that runs when the tool is called.",[27,92457,4737,92458,92464,92465,92467],{},[42,92459,92460,92463],{},[22,92461,92462],{},"typeof navigator === \"undefined\""," guard"," is important. Even though our plugin is client-only, this composable could theoretically be called during SSR if someone uses it incorrectly. The guard prevents a crash by silently returning if ",[22,92466,91452],{}," doesn't exist.",[27,92469,4737,92470,67195,92478,92486],{},[45,92471,92474],{"href":92472,"target":2716,"rel":92473},"https:\u002F\u002Fvuejs.org\u002Fapi\u002Fcomposition-api-lifecycle.html#onmounted",[2718,2719],[42,92475,92476],{},[22,92477,10538],{},[45,92479,92482],{"href":92480,"target":2716,"rel":92481},"https:\u002F\u002Fvuejs.org\u002Fapi\u002Fcomposition-api-lifecycle.html#onunmounted",[2718,2719],[42,92483,92484],{},[22,92485,42018],{}," pattern ties the tool's lifecycle to the component's lifecycle. When the component mounts in the browser, the tool gets registered. When the component unmounts (the user navigates away, the component is conditionally removed, etc.), the tool gets unregistered. This prevents memory leaks and stale tool registrations.",[27,92488,92489],{},"Using it in a component is straightforward:",[128,92491,92493],{"className":13299,"code":92492,"language":13301,"meta":133,"style":133},"\u003Cscript setup lang=\"ts\">\nuseMcpTool({\n    name: \"get-page-title\",\n    description: \"Returns the current page title\",\n    inputSchema: { type: \"object\", properties: {} },\n    execute: async () => ({\n        content: [{ type: \"text\", text: document.title }],\n    }),\n});\n\u003C\u002Fscript>\n",[22,92494,92495,92507,92513,92522,92531,92539,92553,92563,92567,92571],{"__ignoreMap":133},[137,92496,92497,92499,92501,92503,92505],{"class":139,"line":140},[137,92498,4033],{"class":143},[137,92500,29224],{"class":157},[137,92502,253],{"class":143},[137,92504,29848],{"class":284},[137,92506,4053],{"class":143},[137,92508,92509,92511],{"class":139,"line":173},[137,92510,90023],{"class":147},[137,92512,3175],{"class":157},[137,92514,92515,92517,92520],{"class":139,"line":188},[137,92516,57427],{"class":157},[137,92518,92519],{"class":284},"\"get-page-title\"",[137,92521,1961],{"class":157},[137,92523,92524,92526,92529],{"class":139,"line":269},[137,92525,88296],{"class":157},[137,92527,92528],{"class":284},"\"Returns the current page title\"",[137,92530,1961],{"class":157},[137,92532,92533,92535,92537],{"class":139,"line":278},[137,92534,89261],{"class":157},[137,92536,83873],{"class":284},[137,92538,89266],{"class":157},[137,92540,92541,92543,92545,92547,92549,92551],{"class":139,"line":291},[137,92542,88413],{"class":147},[137,92544,726],{"class":157},[137,92546,15050],{"class":143},[137,92548,1484],{"class":157},[137,92550,222],{"class":143},[137,92552,80940],{"class":157},[137,92554,92555,92558,92560],{"class":139,"line":297},[137,92556,92557],{"class":157},"        content: [{ type: ",[137,92559,7837],{"class":284},[137,92561,92562],{"class":157},", text: document.title }],\n",[137,92564,92565],{"class":139,"line":302},[137,92566,73812],{"class":157},[137,92568,92569],{"class":139,"line":662},[137,92570,5422],{"class":157},[137,92572,92573,92575,92577],{"class":139,"line":667},[137,92574,4083],{"class":143},[137,92576,4037],{"class":157},[137,92578,4053],{"class":143},[27,92580,92581,92582,92584,92585,92587],{},"No imports needed. Nuxt auto-imports ",[22,92583,90023],{}," because we registered it with ",[22,92586,90479],{}," in our module setup.",[123,92589,92591,92593],{"id":92590},"usemcpb-manual-lifecycle-control",[22,92592,90026],{}," - Manual Lifecycle Control",[27,92595,92596],{},"For cases where users need direct control over initialisation and cleanup:",[128,92598,92600],{"className":13299,"code":92599,"language":13301,"meta":133,"style":133},"\u002F\u002F src\u002Fruntime\u002Fapp\u002Fcomposables\u002FuseMcpB.ts\nimport { initializeWebModelContext, cleanupWebModelContext } from \"@mcp-b\u002Fglobal\";\nimport type { WebModelContextInitOptions } from \"@mcp-b\u002Fglobal\";\n\nexport function useMcpB() {\n    return {\n        initialize: (options?: WebModelContextInitOptions) => initializeWebModelContext(options),\n        cleanup: () => cleanupWebModelContext(),\n    };\n}\n",[22,92601,92602,92607,92620,92635,92639,92650,92656,92680,92694,92698],{"__ignoreMap":133},[137,92603,92604],{"class":139,"line":140},[137,92605,92606],{"class":308},"\u002F\u002F src\u002Fruntime\u002Fapp\u002Fcomposables\u002FuseMcpB.ts\n",[137,92608,92609,92611,92614,92616,92618],{"class":139,"line":173},[137,92610,10287],{"class":143},[137,92612,92613],{"class":157}," { initializeWebModelContext, cleanupWebModelContext } ",[137,92615,10954],{"class":143},[137,92617,89219],{"class":284},[137,92619,3276],{"class":157},[137,92621,92622,92624,92626,92629,92631,92633],{"class":139,"line":188},[137,92623,10287],{"class":143},[137,92625,25639],{"class":143},[137,92627,92628],{"class":157}," { WebModelContextInitOptions } ",[137,92630,10954],{"class":143},[137,92632,89219],{"class":284},[137,92634,3276],{"class":157},[137,92636,92637],{"class":139,"line":269},[137,92638,516],{"emptyLinePlaceholder":515},[137,92640,92641,92643,92645,92648],{"class":139,"line":278},[137,92642,13456],{"class":143},[137,92644,154],{"class":143},[137,92646,92647],{"class":147}," useMcpB",[137,92649,275],{"class":157},[137,92651,92652,92654],{"class":139,"line":291},[137,92653,176],{"class":143},[137,92655,256],{"class":157},[137,92657,92658,92661,92663,92665,92667,92670,92672,92674,92677],{"class":139,"line":297},[137,92659,92660],{"class":147},"        initialize",[137,92662,51456],{"class":157},[137,92664,90880],{"class":161},[137,92666,35824],{"class":143},[137,92668,92669],{"class":147}," WebModelContextInitOptions",[137,92671,219],{"class":157},[137,92673,222],{"class":143},[137,92675,92676],{"class":147}," initializeWebModelContext",[137,92678,92679],{"class":157},"(options),\n",[137,92681,92682,92685,92687,92689,92692],{"class":139,"line":302},[137,92683,92684],{"class":147},"        cleanup",[137,92686,1683],{"class":157},[137,92688,222],{"class":143},[137,92690,92691],{"class":147}," cleanupWebModelContext",[137,92693,84137],{"class":157},[137,92695,92696],{"class":139,"line":662},[137,92697,1892],{"class":157},[137,92699,92700],{"class":139,"line":667},[137,92701,510],{"class":157},[27,92703,92704,92705,92707],{},"This is a thin wrapper - just five lines of actual code. But it's valuable because it's auto-imported and gives users a clean API without needing to know about ",[22,92706,89099],{},"'s internal exports. A user can simply write:",[128,92709,92711],{"className":13299,"code":92710,"language":13301,"meta":133,"style":133},"\u003Cscript setup lang=\"ts\">\nconst { initialize, cleanup } = useMcpB();\n\n\u002F\u002F Re-initialize with custom options\ninitialize({\n    transport: {\n        tabServer: { allowedOrigins: [\"https:\u002F\u002Fexample.com\"] },\n    },\n});\n\n\u002F\u002F Clean up when done\nonUnmounted(() => cleanup());\n\u003C\u002Fscript>\n",[22,92712,92713,92725,92746,92750,92755,92761,92766,92776,92780,92784,92788,92793,92806],{"__ignoreMap":133},[137,92714,92715,92717,92719,92721,92723],{"class":139,"line":140},[137,92716,4033],{"class":143},[137,92718,29224],{"class":157},[137,92720,253],{"class":143},[137,92722,29848],{"class":284},[137,92724,4053],{"class":143},[137,92726,92727,92729,92731,92733,92735,92738,92740,92742,92744],{"class":139,"line":173},[137,92728,3077],{"class":143},[137,92730,8906],{"class":157},[137,92732,31845],{"class":364},[137,92734,164],{"class":157},[137,92736,92737],{"class":364},"cleanup",[137,92739,8911],{"class":157},[137,92741,253],{"class":143},[137,92743,92647],{"class":147},[137,92745,924],{"class":157},[137,92747,92748],{"class":139,"line":188},[137,92749,516],{"emptyLinePlaceholder":515},[137,92751,92752],{"class":139,"line":269},[137,92753,92754],{"class":308},"\u002F\u002F Re-initialize with custom options\n",[137,92756,92757,92759],{"class":139,"line":278},[137,92758,31845],{"class":147},[137,92760,3175],{"class":157},[137,92762,92763],{"class":139,"line":291},[137,92764,92765],{"class":157},"    transport: {\n",[137,92767,92768,92771,92774],{"class":139,"line":297},[137,92769,92770],{"class":157},"        tabServer: { allowedOrigins: [",[137,92772,92773],{"class":284},"\"https:\u002F\u002Fexample.com\"",[137,92775,67659],{"class":157},[137,92777,92778],{"class":139,"line":302},[137,92779,775],{"class":157},[137,92781,92782],{"class":139,"line":662},[137,92783,5422],{"class":157},[137,92785,92786],{"class":139,"line":667},[137,92787,516],{"emptyLinePlaceholder":515},[137,92789,92790],{"class":139,"line":786},[137,92791,92792],{"class":308},"\u002F\u002F Clean up when done\n",[137,92794,92795,92797,92799,92801,92804],{"class":139,"line":798},[137,92796,42018],{"class":147},[137,92798,3193],{"class":157},[137,92800,222],{"class":143},[137,92802,92803],{"class":147}," cleanup",[137,92805,14173],{"class":157},[137,92807,92808,92810,92812],{"class":139,"line":803},[137,92809,4083],{"class":143},[137,92811,4037],{"class":157},[137,92813,4053],{"class":143},[104,92815,92817],{"id":92816},"step-5-the-development-playground","Step 5: The Development Playground",[27,92819,92820,92821,92824],{},"We need a way to try our module during development. Nuxt modules typically include a ",[22,92822,92823],{},"playground\u002F"," directory - a minimal Nuxt application that imports the module directly from source.",[128,92826,92828],{"className":13299,"code":92827,"language":13301,"meta":133,"style":133},"\u002F\u002F playground\u002Fnuxt.config.ts\nexport default defineNuxtConfig({\n    modules: [\"..\u002Fsrc\u002Fmodule\"],\n    mcpB: {\n        autoInitialize: true,\n    },\n    compatibilityDate: \"2025-01-01\",\n});\n",[22,92829,92830,92835,92845,92854,92858,92866,92870,92880],{"__ignoreMap":133},[137,92831,92832],{"class":139,"line":140},[137,92833,92834],{"class":308},"\u002F\u002F playground\u002Fnuxt.config.ts\n",[137,92836,92837,92839,92841,92843],{"class":139,"line":173},[137,92838,13456],{"class":143},[137,92840,21723],{"class":143},[137,92842,21726],{"class":147},[137,92844,3175],{"class":157},[137,92846,92847,92849,92852],{"class":139,"line":188},[137,92848,91204],{"class":157},[137,92850,92851],{"class":284},"\"..\u002Fsrc\u002Fmodule\"",[137,92853,21916],{"class":157},[137,92855,92856],{"class":139,"line":269},[137,92857,91213],{"class":157},[137,92859,92860,92862,92864],{"class":139,"line":278},[137,92861,90843],{"class":157},[137,92863,3097],{"class":364},[137,92865,1961],{"class":157},[137,92867,92868],{"class":139,"line":291},[137,92869,775],{"class":157},[137,92871,92872,92875,92878],{"class":139,"line":297},[137,92873,92874],{"class":157},"    compatibilityDate: ",[137,92876,92877],{"class":284},"\"2025-01-01\"",[137,92879,1961],{"class":157},[137,92881,92882],{"class":139,"line":302},[137,92883,5422],{"class":157},[128,92885,92887],{"className":4024,"code":92886,"language":4026,"meta":133,"style":133},"\u003C!-- playground\u002Fapp.vue -->\n\u003Ctemplate>\n    \u003Cdiv>\n        \u003Ch1>nuxt-mcp-b Playground\u003C\u002Fh1>\n        \u003Cp>Open the browser console to verify the module is working.\u003C\u002Fp>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[22,92888,92889,92894,92902,92910,92923,92936,92944],{"__ignoreMap":133},[137,92890,92891],{"class":139,"line":140},[137,92892,92893],{"class":308},"\u003C!-- playground\u002Fapp.vue -->\n",[137,92895,92896,92898,92900],{"class":139,"line":173},[137,92897,4033],{"class":157},[137,92899,7821],{"class":4036},[137,92901,4053],{"class":157},[137,92903,92904,92906,92908],{"class":139,"line":188},[137,92905,4072],{"class":157},[137,92907,8330],{"class":4036},[137,92909,4053],{"class":157},[137,92911,92912,92914,92916,92919,92921],{"class":139,"line":269},[137,92913,9826],{"class":157},[137,92915,17],{"class":4036},[137,92917,92918],{"class":157},">nuxt-mcp-b Playground\u003C\u002F",[137,92920,17],{"class":4036},[137,92922,4053],{"class":157},[137,92924,92925,92927,92929,92932,92934],{"class":139,"line":278},[137,92926,9826],{"class":157},[137,92928,27],{"class":4036},[137,92930,92931],{"class":157},">Open the browser console to verify the module is working.\u003C\u002F",[137,92933,27],{"class":4036},[137,92935,4053],{"class":157},[137,92937,92938,92940,92942],{"class":139,"line":291},[137,92939,8374],{"class":157},[137,92941,8330],{"class":4036},[137,92943,4053],{"class":157},[137,92945,92946,92948,92950],{"class":139,"line":297},[137,92947,4083],{"class":157},[137,92949,7821],{"class":4036},[137,92951,4053],{"class":157},[27,92953,90538,92954,92957,92958,92961],{},[22,92955,92956],{},"npm run dev:prepare"," once to stub the module, then ",[22,92959,92960],{},"npm run dev"," to start the playground. You'll have a live Nuxt app running your module - any changes you make to the source files will reflect immediately.",[104,92963,92965],{"id":92964},"why-build-a-module-instead-of-just-using-the-library-directly","Why Build a Module Instead of Just Using the Library Directly?",[27,92967,92968,92969,92972],{},"This is a fair question. You could absolutely just ",[22,92970,92971],{},"npm install @mcp-b\u002Fglobal"," and write a plugin manually. Here's what a module gives you on top of that:",[1003,92974,92975,92984,92994,93000,93009],{},[1006,92976,92977,92980,92981,92983],{},[42,92978,92979],{},"Zero-config setup"," - users add one line to their ",[22,92982,89896],{}," array and everything works. No plugin files to create, no composables to manually import",[1006,92985,92986,76073,92989,114,92991,92993],{},[42,92987,92988],{},"Auto-imported composables",[22,92990,90023],{},[22,92992,90026],{}," are available everywhere without import statements",[1006,92995,92996,92999],{},[42,92997,92998],{},"Proper SSR handling"," - the module ensures client-only code never runs on the server. Users don't have to think about this",[1006,93001,93002,93005,93006,93008],{},[42,93003,93004],{},"Centralised configuration"," - all options live in ",[22,93007,21711],{}," alongside everything else, with TypeScript autocomplete",[1006,93010,93011,93014,93015,93019,93020],{},[42,93012,93013],{},"Ecosystem discoverability"," - published modules appear on ",[45,93016,93018],{"href":89993,"target":2716,"rel":93017},[2718,2719],"nuxt.com\u002Fmodules"," and can be installed with ",[22,93021,93022],{},"npx nuxi module add",[27,93024,93025,93026,114,93028,93031],{},"The difference between ",[22,93027,92971],{},[22,93029,93030],{},"npm install nuxt-mcp-b"," is the difference between \"here's a library, figure out how to integrate it\" and \"add this to your modules array and you're done.\"",[104,93033,93035],{"id":93034},"patterns-worth-remembering","Patterns Worth Remembering",[27,93037,93038],{},"If you're planning to build your own Nuxt module, here are the patterns I found most important:",[2569,93040,93041,93049,93063,93077,93089],{},[1006,93042,93043,93048],{},[42,93044,9726,93045],{},[22,93046,93047],{},"createResolver(import.meta.url)"," - always resolve paths relative to your module file, not the user's project root. This is the number one thing that will bite you if you forget it",[1006,93050,93051,93057,93058,9607,93060,93062],{},[42,93052,93053,93054,93056],{},"Client-only code goes in ",[22,93055,91863],{}," files"," - and use ",[22,93059,91442],{},[22,93061,90476],{}," for extra safety. Belt and braces",[1006,93064,93065,93070,93071,93073,93074,93076],{},[42,93066,93067,93068],{},"Pass build-time options to runtime via ",[22,93069,90134],{}," - the ",[22,93072,15609],{}," function and runtime code live in different worlds. ",[22,93075,91391],{}," is the bridge between them",[1006,93078,93079,76073,93082,93084,93085,93088],{},[42,93080,93081],{},"Prefix your composables",[22,93083,90023],{}," is better than ",[22,93086,93087],{},"useTool"," to avoid naming conflicts with other modules or the user's own code",[1006,93090,93091,93094,93095,93098],{},[42,93092,93093],{},"Provide sensible defaults"," - most users should get a working module without any configuration. If ",[22,93096,93097],{},"autoInitialize: true"," is the common case, make it the default",[104,93100,93102],{"id":93101},"automating-the-module-with-agentic-workflows","Automating the Module with Agentic Workflows",[27,93104,93105],{},"Building the module is one thing. Keeping it alive is another.",[27,93107,93108,93109,93112],{},"Once ",[22,93110,93111],{},"nuxt-mcp-b"," was published on npm, I immediately ran into the same maintenance burden every open-source author knows: version bumps, release scripts, checking whether the upstream library has changed, updating dependencies. None of it is difficult - it's just tedious and easy to forget. So I automated it.",[27,93114,93115,93116,93118,93119,93121,93122,39395],{},"I used ",[47718,93117,84512],{"to":88074}," - a relatively new feature that lets you write GitHub Actions in plain Markdown instead of YAML. Instead of scripting every step yourself, you describe what you want in natural language and an AI agent figures out how to do it. The workflow files live in ",[22,93120,76051],{}," just like regular Actions, but with a ",[22,93123,80806],{},[27,93125,93126,93127,93130],{},"I wrote ",[47718,93128,93129],{"to":88074},"a separate article"," explaining how agentic workflows work in detail - the frontmatter configuration, safe outputs, compilation, security model, and a full set of practical examples. If you're new to the concept, I'd recommend reading that first. Here, I'll focus on the two specific workflows I created for this module and why.",[123,93132,93134],{"id":93133},"auto-release","Auto Release",[27,93136,93137,93138,93140],{},"The first workflow triggers every time code is pushed to the ",[22,93139,58927],{}," branch. The agent reads the recent commit history, inspects what changed, and determines whether a new release is needed - and if so, whether it should be a patch, minor, or major version bump.",[27,93142,93143],{},"The decision follows standard semantic versioning rules. If someone changed a composable's public API in a breaking way, that's a major bump. A new configuration option or a new composable is a minor bump. A bug fix, a dependency update, or a refactor that doesn't touch the public surface is a patch. And if the only changes are to markdown files or formatting configuration, the agent skips the release entirely - there's nothing new to publish.",[27,93145,93146,93147,93149,93150,93152],{},"When a release is warranted, the agent edits ",[22,93148,5140],{}," to bump the version, runs the build and publish script inside a sandboxed environment using the npm token, and then opens a pull request back to ",[22,93151,58927],{}," with the version change. That PR keeps the repository in sync with what was published. The whole process happens without any manual intervention.",[27,93154,93155,93156,93158],{},"The key thing that makes this safe is the ",[42,93157,84649],{}," model. The agent itself never gets direct write access to the repository. It can read code, edit files in its sandbox, and run commands - but the only way it can affect the outside world is through pre-approved channels: creating a pull request or opening an issue. If the publish fails, it creates an issue reporting the problem instead. There's no scenario where the agent silently breaks something.",[123,93160,93162],{"id":93161},"monthly-dependency-monitor","Monthly Dependency Monitor",[27,93164,93165,93166,93168],{},"The second workflow runs on a schedule - once a month, on the first of the month. Its job is to check whether the upstream ",[22,93167,89099],{}," package has released new features, breaking changes, or updates that our module needs to accommodate.",[27,93170,93171,93172,34894,93176,93180,93181,93183,93184,93189,93190,93195,93196,93198,93199,93201],{},"The agent checks four sources: the ",[45,93173,93175],{"href":89903,"target":2716,"rel":93174},[2718,2719],"npm package page",[45,93177,71386],{"href":93178,"target":2716,"rel":93179},"https:\u002F\u002Fgithub.com\u002FWebMCP-org\u002Fnpm-packages",[2718,2719]," where ",[22,93182,89099],{}," is developed, the ",[45,93185,93188],{"href":93186,"target":2716,"rel":93187},"https:\u002F\u002Fgithub.com\u002FWebMCP-org\u002Fnpm-packages\u002Fblob\u002Fmain\u002Fpackages\u002Fglobal\u002FCHANGELOG.md",[2718,2719],"changelog"," for the package, and the ",[45,93191,93194],{"href":93192,"target":2716,"rel":93193},"https:\u002F\u002Fdocs.mcp-b.ai\u002Fpackages\u002Fglobal\u002Foverview",[2718,2719],"official documentation",". It compares what it finds against our current implementation - our ",[22,93197,90102],{},", our plugin, our composables, our ",[22,93200,5140],{}," version constraint.",[27,93203,93204,93205,93207],{},"If code changes are needed - say ",[22,93206,89099],{}," added a new configuration option we should expose, or changed a function signature we rely on - the agent creates a pull request with the necessary updates. I'm set as a reviewer on the PR, so I get notified and can review the changes before merging.",[27,93209,93210],{},"If there's a new upstream version but it doesn't affect our module (internal changes, bug fixes in code paths we don't touch), the agent creates an issue documenting what it found and why no code changes are needed. And if nothing has changed at all, it does nothing.",[27,93212,93213],{},"This workflow has been genuinely useful. Before setting it up, I'd occasionally check the upstream repo manually and wonder if I'd missed anything. Now I don't have to think about it - the monitor does the checking and tells me when something needs my attention.",[123,93215,93217],{"id":93216},"why-this-matters-for-module-authors","Why This Matters for Module Authors",[27,93219,93220],{},"If you're publishing an npm package that wraps an external library, you're effectively taking on a dependency relationship with that library's maintainers. When they ship updates, your users expect your module to keep up. Automating both the release process and the dependency monitoring means you can maintain a module responsibly without it becoming a second job.",[27,93222,93223,93224,1017],{},"The agentic workflows approach is particularly well-suited to this because the instructions are written in plain English. You don't need to learn a specialised CI scripting language - you describe the logic (\"analyse commits, determine semver bump, publish if needed\") and the agent handles the implementation. If you want to set this up for your own module, I covered the full technical details - frontmatter configuration, safe outputs, compilation, and more - in my ",[47718,93225,93226],{"to":88074},"agentic workflows article",[104,93228,2567],{"id":2566},[27,93230,93231,93232,93234,93235,93240,93241,93243,93244,93249],{},"Building a Nuxt module is more approachable than it looks. At its core, it's a ",[22,93233,15609],{}," function that registers plugins, composables, and configuration using the utilities from ",[45,93236,93238],{"href":90464,"target":2716,"rel":93237},[2718,2719],[22,93239,90196],{},". The architecture follows a clear pattern: module definition at build time, runtime code at application time, and ",[22,93242,90134],{}," bridging the two. If you want to dive deeper, the official ",[45,93245,93248],{"href":93246,"target":2716,"rel":93247},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Fgoing-further\u002Fmodules",[2718,2719],"Nuxt Module Author Guide"," is an excellent next step.",[27,93251,93252],{},"What I find most satisfying about this process is the shift in perspective. You stop thinking about \"how do I use this library in my project?\" and start thinking about \"how would I want someone to use this library?\" That mindset produces better APIs and better developer experiences - whether you're wrapping an existing package or building something entirely new.",[27,93254,93255],{},"And once you've built it, automating the maintenance with agentic workflows means the module stays healthy without constant manual attention. The combination of a well-structured module and smart automation is what turns a side project into something you can confidently publish and maintain.",[27,93257,93258,93259,93261,93262,93266,93267,1017],{},"You can find the complete source code for ",[22,93260,93111],{}," on ",[45,93263,50876],{"href":93264,"target":2716,"rel":93265},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fnuxt-mcp-b",[2718,2719],", and the published module on ",[45,93268,9536],{"href":93269,"target":2716,"rel":93270},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fnuxt-mcp-b",[2718,2719],[2617,93272,93273],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":133,"searchDepth":173,"depth":173,"links":93275},[93276,93277,93278,93279,93280,93281,93282,93283,93293,93294,93300,93301,93302,93303,93308],{"id":89913,"depth":173,"text":89914},{"id":89933,"depth":173,"text":89934},{"id":59180,"depth":173,"text":59181},{"id":66828,"depth":173,"text":66831},{"id":90081,"depth":173,"text":90082},{"id":90138,"depth":173,"text":90139},{"id":90180,"depth":173,"text":90181},{"id":90545,"depth":173,"text":90546,"children":93284},[93285,93287,93289,93291],{"id":91093,"depth":188,"text":93286},"The meta Object",{"id":91242,"depth":188,"text":93288},"The defaults Object",{"id":91306,"depth":188,"text":93290},"The setup Function",{"id":91546,"depth":188,"text":93292},"Why createResolver Matters",{"id":91608,"depth":173,"text":91609},{"id":91941,"depth":173,"text":91942,"children":93295},[93296,93298],{"id":91952,"depth":188,"text":93297},"useMcpTool - Register Tools with Lifecycle Management",{"id":92590,"depth":188,"text":93299},"useMcpB - Manual Lifecycle Control",{"id":92816,"depth":173,"text":92817},{"id":92964,"depth":173,"text":92965},{"id":93034,"depth":173,"text":93035},{"id":93101,"depth":173,"text":93102,"children":93304},[93305,93306,93307],{"id":93133,"depth":188,"text":93134},{"id":93161,"depth":188,"text":93162},{"id":93216,"depth":188,"text":93217},{"id":2566,"depth":173,"text":2567},"A step-by-step guide to building a real, publishable Nuxt module from scratch, covering module architecture, client-side plugins, auto-imported composables, TypeScript support, and automation with agentic workflows.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1773227493\u002Fblog\u002Fhow-to-create-a-nuxt-module-a-beginner-friendly-guide\u002Fhow-to-create-a-nuxt-module-a-beginner-friendly-guide-2_kn1xkg",[93312,21096,8,93313,93314,90473,93315,89978,93316,90134,93317,93318,93319,12817,93320,89968,88125,89836,93321,80385,93322,93323],"Nuxt module","Nuxt module development","nuxt\u002Fkit","Nuxt plugin","composables","client-side plugin","module builder","npm package","SSR","agentic workflows","nuxi","Nuxt ecosystem",{},"\u002F2026\u002F03\u002F15\u002Fhow-to-create-a-nuxt-module-a-beginner-friendly-guide","15th March 2026",{"title":89849,"description":93309},"2026\u002F03\u002F15\u002Fhow-to-create-a-nuxt-module-a-beginner-friendly-guide","uVFTJqllrV0WUW3AnG0YW05M4X9mPE1btVpmGhDaHDg",{"id":93331,"title":93332,"articleTags":93333,"author":11,"blog":12,"body":93334,"description":94313,"extension":2649,"image":94314,"keywords":94315,"meta":94335,"navigation":515,"path":94336,"published":94337,"readTime":786,"seo":94338,"stem":94339,"type":2662,"__hash__":94340},"content\u002F2026\u002F03\u002F22\u002Fi-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer.md","I Gave My Claude Code the Personality of a Sarcastic Senior Developer",[27886,71449,71450],{"type":14,"value":93335,"toc":94303},[93336,93339,93353,93355,93359,93364,93379,93384,93391,93399,93401,93405,93415,93421,93424,93430,93440,93447,93592,93601,93604,93610,93613,93619,93625,93629,93632,93639,93645,93656,93659,93665,93672,93678,93695,93699,93702,93709,93715,93720,93816,93831,93847,93859,93863,93872,93877,93883,93889,93894,93973,93983,93986,94004,94008,94015,94025,94031,94034,94054,94061,94067,94073,94079,94082,94088,94094,94098,94105,94111,94121,94124,94130,94133,94137,94148,94155,94167,94170,94176,94182,94188,94198,94204,94207,94221,94234,94236,94239,94242,94245,94288,94300],[17,93337,93332],{"id":93338},"i-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer",[27,93340,93341],{},[30,93342,93343,36,93345,88096,93347],{},[33,93344],{"value":35},[33,93346],{"value":39},[42,93348,93349],{},[45,93350,93351],{"href":47},[33,93352],{"value":50},[52,93354],{":tags":54},[56,93356],{":audio-src":93357,":transcript-src":93358},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F03\u002F22\u002Fi-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F03\u002F22\u002Fi-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer\u002Fsummary.json",[27,93360,93361],{},[63,93362],{"alt":12847,"src":93363},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1773917254\u002Fblog\u002Fi-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer\u002Fi-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer_jpghb5",[27,93365,93366,93367,93370,93371,93374,93375,93378],{},"What if you could make your Claude Code a bit more... playful? Give it the character of a joking senior engineer who's been through too many code reviews and has developed a dry sense of humour about the whole thing. Instead of watching \"",[22,93368,93369],{},"Thinking...","\" spin in your terminal, you'd see \"",[22,93372,93373],{},"Blaming the previous developer (it was me)...","\" or \"",[22,93376,93377],{},"Convincing myself this is clean code...","\" 🙃.",[3244,93380,93381],{},[27,93382,93383],{},"A quick note before we start: everything in this article I did purely for fun. The customisations are genuinely useful, but the sarcastic senior dev theme is just my personal flavour. If any of it feels like too much, you absolutely don't need to stick with it - swap in your own style, tone it down, or just cherry-pick the parts that make sense for your workflow.",[27,93385,93386,93387,93390],{},"Turns out, you can. Claude Code lets you customise almost everything about its personality - the loading messages, the tips it shows you, how it formats responses, even the sound it makes when it's done. And the best part? You don't edit any config files. You just tell Claude Code what you want in plain English - or even say it out loud using ",[22,93388,93389],{},"\u002Fvoice"," mode - and it configures itself.",[27,93392,93393,93394,93398],{},"In this article, I'll walk you through a collection of customisations that turned my Claude Code from a generic terminal tool into something with genuine character - part helpful colleague, part code reviewer who's had one too many coffees. Pick the ones that appeal to you, skip the rest, or do them all. I've put all the configuration examples, scripts, and skills from this article into a ",[45,93395,71386],{"href":93396,"target":2716,"rel":93397},"https:\u002F\u002Fgithub.com\u002FSuv4o\u002Fclaude-code-setting-of-the-personality-of-a-sarcastic-senior-developer",[2718,2719]," so you can browse the full setup or use it as a starting point for your own.",[27,93400,75994],{},[104,93402,93404],{"id":93403},"give-it-a-sense-of-humour-custom-spinner-verbs","Give It a Sense of Humour (Custom Spinner Verbs)",[27,93406,93407,93408,93410,93411,93414],{},"This is where the personality starts. When Claude Code is thinking, it shows a spinner with rotating verbs like \"",[22,93409,93369],{},"\" and \"",[22,93412,93413],{},"Reasoning...","\". Functional, sure. But boring. You can replace these with whatever you want - and this is where things get fun.",[27,93416,93417],{},[63,93418],{"alt":93419,"src":93420},"Custom Spinner Verbs","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1773917286\u002Fblog\u002Fi-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer\u002Fcustom-spinner-verbs_a8kgi6",[27,93422,93423],{},"I wanted mine to feel like a developer who's been through too many sprint retrospectives and has developed a dark sense of humour about it. Open Claude Code and type - or say - something like this:",[128,93425,93428],{"className":93426,"code":93427,"language":5189},[5187],"Replace my spinner verbs with these: \"Bikeshedding the variable names\", \"Promising this is the last refactor\", \"Googling my own blog post\", \"Blaming the previous developer (it was me)\", \"Adding TODO comments I'll never revisit\", \"Convincing myself this is clean code\", \"Rubber-ducking with myself\", \"Pretending I read the docs\", \"Over-engineering the simple part\", \"Ignoring the TypeScript errors\", \"Pushing directly to main\", \"Writing a test after the bug\"\n",[22,93429,93427],{"__ignoreMap":133},[3244,93431,93432],{},[27,93433,93434,93436,93437,93439],{},[42,93435,45255],{}," Every prompt in this article can be spoken instead of typed. Run ",[22,93438,93389],{}," to enable voice mode, then hold the spacebar and talk. Claude Code transcribes your speech in real time and processes it just like a typed prompt. It's especially handy for the longer customisation prompts in this article - describing what you want out loud is often faster than writing it out.",[27,93441,93442,93443,93446],{},"Claude Code will update your ",[22,93444,93445],{},"~\u002F.claude\u002Fsettings.json"," with something like this:",[128,93448,93450],{"className":5155,"code":93449,"language":5157,"meta":133,"style":133},"{\n    \"spinnerVerbs\": {\n        \"mode\": \"replace\",\n        \"verbs\": [\n            \"Bikeshedding the variable names\",\n            \"Promising this is the last refactor\",\n            \"Googling my own blog post\",\n            \"Blaming the previous developer (it was me)\",\n            \"Adding TODO comments I'll never revisit\",\n            \"Convincing myself this is clean code\",\n            \"Rubber-ducking with myself\",\n            \"Pretending I read the docs\",\n            \"Over-engineering the simple part\",\n            \"Ignoring the TypeScript errors\",\n            \"Pushing directly to main\",\n            \"Writing a test after the bug\",\n            \"Amending my mistakes\",\n            \"Pulling all-nighter\"\n        ]\n    }\n}\n",[22,93451,93452,93456,93463,93475,93483,93490,93497,93504,93511,93518,93525,93532,93539,93546,93553,93560,93567,93574,93579,93584,93588],{"__ignoreMap":133},[137,93453,93454],{"class":139,"line":140},[137,93455,15971],{"class":157},[137,93457,93458,93461],{"class":139,"line":173},[137,93459,93460],{"class":364},"    \"spinnerVerbs\"",[137,93462,1819],{"class":157},[137,93464,93465,93468,93470,93473],{"class":139,"line":188},[137,93466,93467],{"class":364},"        \"mode\"",[137,93469,726],{"class":157},[137,93471,93472],{"class":284},"\"replace\"",[137,93474,1961],{"class":157},[137,93476,93477,93480],{"class":139,"line":269},[137,93478,93479],{"class":364},"        \"verbs\"",[137,93481,93482],{"class":157},": [\n",[137,93484,93485,93488],{"class":139,"line":278},[137,93486,93487],{"class":284},"            \"Bikeshedding the variable names\"",[137,93489,1961],{"class":157},[137,93491,93492,93495],{"class":139,"line":291},[137,93493,93494],{"class":284},"            \"Promising this is the last refactor\"",[137,93496,1961],{"class":157},[137,93498,93499,93502],{"class":139,"line":297},[137,93500,93501],{"class":284},"            \"Googling my own blog post\"",[137,93503,1961],{"class":157},[137,93505,93506,93509],{"class":139,"line":302},[137,93507,93508],{"class":284},"            \"Blaming the previous developer (it was me)\"",[137,93510,1961],{"class":157},[137,93512,93513,93516],{"class":139,"line":662},[137,93514,93515],{"class":284},"            \"Adding TODO comments I'll never revisit\"",[137,93517,1961],{"class":157},[137,93519,93520,93523],{"class":139,"line":667},[137,93521,93522],{"class":284},"            \"Convincing myself this is clean code\"",[137,93524,1961],{"class":157},[137,93526,93527,93530],{"class":139,"line":786},[137,93528,93529],{"class":284},"            \"Rubber-ducking with myself\"",[137,93531,1961],{"class":157},[137,93533,93534,93537],{"class":139,"line":798},[137,93535,93536],{"class":284},"            \"Pretending I read the docs\"",[137,93538,1961],{"class":157},[137,93540,93541,93544],{"class":139,"line":803},[137,93542,93543],{"class":284},"            \"Over-engineering the simple part\"",[137,93545,1961],{"class":157},[137,93547,93548,93551],{"class":139,"line":931},[137,93549,93550],{"class":284},"            \"Ignoring the TypeScript errors\"",[137,93552,1961],{"class":157},[137,93554,93555,93558],{"class":139,"line":1568},[137,93556,93557],{"class":284},"            \"Pushing directly to main\"",[137,93559,1961],{"class":157},[137,93561,93562,93565],{"class":139,"line":1573},[137,93563,93564],{"class":284},"            \"Writing a test after the bug\"",[137,93566,1961],{"class":157},[137,93568,93569,93572],{"class":139,"line":1578},[137,93570,93571],{"class":284},"            \"Amending my mistakes\"",[137,93573,1961],{"class":157},[137,93575,93576],{"class":139,"line":1588},[137,93577,93578],{"class":284},"            \"Pulling all-nighter\"\n",[137,93580,93581],{"class":139,"line":1601},[137,93582,93583],{"class":157},"        ]\n",[137,93585,93586],{"class":139,"line":3802},[137,93587,294],{"class":157},[137,93589,93590],{"class":139,"line":3808},[137,93591,510],{"class":157},[27,93593,93594,93595,93597,93598,93600],{},"There are two modes worth knowing about here. Using ",[22,93596,33919],{}," swaps out the default verbs entirely - you'll only ever see your custom ones. If you'd prefer your verbs mixed in with the defaults, use ",[22,93599,3816],{}," instead.",[27,93602,93603],{},"One thing to note: the list is static. Whatever verbs you provide, those are exactly the ones Claude Code will cycle through. It won't generate new ones on its own. So if you want more variety, you have two options. Either write a longer list upfront, or - and this is the more fun approach - don't write them yourself at all. Just tell Claude Code something like:",[128,93605,93608],{"className":93606,"code":93607,"language":5189},[5187],"Replace my spinner verbs with 30 verbs that sound like a developer who's had too much coffee and too many merge conflicts\n",[22,93609,93607],{"__ignoreMap":133},[27,93611,93612],{},"Claude will generate the entire list for you. You can also come back later and append more as inspiration strikes:",[128,93614,93617],{"className":93615,"code":93616,"language":5189},[5187],"Append these spinner verbs: \"Explaining Vue reactivity to a React developer\", \"Lubing the keyboard switches again\"\n",[22,93618,93616],{"__ignoreMap":133},[27,93620,93621,93622,93624],{},"Watching \"",[22,93623,93373],{},"\" spin while Claude thinks about my code genuinely makes me laugh every time. It's the little things.",[104,93626,93628],{"id":93627},"give-it-a-dashboard-live-status-line","Give It a Dashboard (Live Status Line)",[27,93630,93631],{},"Alright, this one is less about comedy and more about situational awareness. The status line is a shell script that runs after every Claude turn and displays live information at the bottom of your terminal - a persistent dashboard that's always visible.",[27,93633,93634,93635,93638],{},"The quickest way to set one up is with the ",[22,93636,93637],{},"\u002Fstatusline"," command inside Claude Code. It'll ask what you want to display and generate the script for you. But since we're building a sarcastic senior dev, I wanted something with a bit more personality. Here's what I asked Claude Code to build:",[128,93640,93643],{"className":93641,"code":93642,"language":5189},[5187],"Create a status line script that shows: the active model name, session duration (how long I've been in this session), total tokens used so far, and a \"burnout meter\" label that changes based on session length - \"Fresh\" for under 15 minutes, \"Warmed up\" for 15-45 minutes, \"In the zone\" for 45-90 minutes, and \"Touch grass\" for anything over 90 minutes. Save it to ~\u002F.claude\u002Fstatusline-command.sh, make it executable, and set it as my status line in user settings.\n",[22,93644,93642],{"__ignoreMap":133},[27,93646,93647,93648,93651,93652,93655],{},"Claude Code generates a bash script that receives a JSON object on ",[22,93649,93650],{},"stdin"," with your full session data - model name, token counts, session timing, working directory, and more. The script parses this with ",[22,93653,93654],{},"jq"," and outputs whatever you want with ANSI colour codes.",[27,93657,93658],{},"My status line looks something like this in practice:",[27,93660,93661],{},[63,93662],{"alt":93663,"src":93664},"Live Status Line","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1773917287\u002Fblog\u002Fi-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer\u002Fgive-it-a-dashboard-live-status-line_wfmka1",[27,93666,93667,93668,93671],{},"It's a small thing, but seeing \"",[22,93669,93670],{},"Touch grass","\" appear at the bottom of my terminal after two hours of non-stop prompting is exactly the kind of reminder a sarcastic senior dev would give you. It's also genuinely useful - the token count tells me how expensive the session has been, and the model name reminds me whether I'm on Sonnet or Opus (which matters when you're debugging something tricky and want the heavier model).",[27,93673,93674,93675,93677],{},"The status line script has access to everything in the session JSON payload: working directory, git info, context window stats, total input and output tokens, cache usage, and more. You can combine whatever matters to you into a single line. The ",[22,93676,93637],{}," command is the easiest way to get started - tell it what data you care about and Claude will write the script.",[27,93679,93680,93681,93684,93685,93688,93689,93692,93693,1017],{},"If you ever want to get rid of it, just run ",[22,93682,93683],{},"\u002Fstatusline remove"," (or ",[22,93686,93687],{},"\u002Fstatusline clear",") and Claude Code will clean it up. You can also do it manually by removing the ",[22,93690,93691],{},"\"statusLine\": { ... }"," block from ",[22,93694,93445],{},[104,93696,93698],{"id":93697},"give-it-a-voice-play-a-sound-when-its-done","Give It a Voice (Play a Sound When It's Done)",[27,93700,93701],{},"Every sarcastic senior dev has impeccable timing. And nothing ruins the flow more than staring at a terminal wondering \"is it done yet?\". This customisation changed how I work with Claude Code entirely. I kick off a task, switch to another terminal or browser tab, and hear a sound when Claude's done. The shift from \"sitting and waiting\" to \"multitasking and getting pinged\" is significant.",[27,93703,93704,93705,93708],{},"You can set this up interactively with ",[22,93706,93707],{},"\u002Fhooks",", or just tell Claude Code directly. On macOS:",[128,93710,93713],{"className":93711,"code":93712,"language":5189},[5187],"Set up a Stop hook in my user settings that plays a notification sound when you finish responding. Use afplay on macOS.\n",[22,93714,93712],{"__ignoreMap":133},[27,93716,93717,93718,894],{},"Claude Code will add something like this to your ",[22,93719,93445],{},[128,93721,93723],{"className":5155,"code":93722,"language":5157,"meta":133,"style":133},"{\n    \"hooks\": {\n        \"Stop\": [\n            {\n                \"matcher\": \"*\",\n                \"hooks\": [\n                    {\n                        \"type\": \"command\",\n                        \"command\": \"\u002Fusr\u002Fbin\u002Fafplay \u002FSystem\u002FLibrary\u002FSounds\u002FGlass.aiff\"\n                    }\n                ]\n            }\n        ]\n    }\n}\n",[22,93724,93725,93729,93736,93743,93747,93759,93766,93770,93781,93791,93795,93800,93804,93808,93812],{"__ignoreMap":133},[137,93726,93727],{"class":139,"line":140},[137,93728,15971],{"class":157},[137,93730,93731,93734],{"class":139,"line":173},[137,93732,93733],{"class":364},"    \"hooks\"",[137,93735,1819],{"class":157},[137,93737,93738,93741],{"class":139,"line":188},[137,93739,93740],{"class":364},"        \"Stop\"",[137,93742,93482],{"class":157},[137,93744,93745],{"class":139,"line":269},[137,93746,81549],{"class":157},[137,93748,93749,93752,93754,93757],{"class":139,"line":278},[137,93750,93751],{"class":364},"                \"matcher\"",[137,93753,726],{"class":157},[137,93755,93756],{"class":284},"\"*\"",[137,93758,1961],{"class":157},[137,93760,93761,93764],{"class":139,"line":291},[137,93762,93763],{"class":364},"                \"hooks\"",[137,93765,93482],{"class":157},[137,93767,93768],{"class":139,"line":297},[137,93769,69183],{"class":157},[137,93771,93772,93774,93776,93779],{"class":139,"line":302},[137,93773,83944],{"class":364},[137,93775,726],{"class":157},[137,93777,93778],{"class":284},"\"command\"",[137,93780,1961],{"class":157},[137,93782,93783,93786,93788],{"class":139,"line":662},[137,93784,93785],{"class":364},"                        \"command\"",[137,93787,726],{"class":157},[137,93789,93790],{"class":284},"\"\u002Fusr\u002Fbin\u002Fafplay \u002FSystem\u002FLibrary\u002FSounds\u002FGlass.aiff\"\n",[137,93792,93793],{"class":139,"line":667},[137,93794,63090],{"class":157},[137,93796,93797],{"class":139,"line":786},[137,93798,93799],{"class":157},"                ]\n",[137,93801,93802],{"class":139,"line":798},[137,93803,760],{"class":157},[137,93805,93806],{"class":139,"line":803},[137,93807,93583],{"class":157},[137,93809,93810],{"class":139,"line":931},[137,93811,294],{"class":157},[137,93813,93814],{"class":139,"line":1568},[137,93815,510],{"class":157},[27,93817,93818,93819,93822,93823,93826,93827,93830],{},"Hooks are shell commands that execute automatically at specific points in Claude Code's lifecycle. The ",[22,93820,93821],{},"Stop"," hook fires when Claude finishes a response - but it's just one of many lifecycle events you can hook into. ",[22,93824,93825],{},"afplay"," is macOS's built-in audio player, and you'll find a collection of system sounds in ",[22,93828,93829],{},"\u002FSystem\u002FLibrary\u002FSounds\u002F"," - just swap the filename to pick one you like.",[27,93832,93833,93834,93836,93837,164,93840,29088,93843,93846],{},"You're not limited to system sounds either. ",[22,93835,93825],{}," can play any ",[22,93838,93839],{},".aiff",[22,93841,93842],{},".mp3",[22,93844,93845],{},".wav"," file, so you can point it at any audio file you like.",[27,93848,93849,93850,73960,93852,3955,93855,93858],{},"If you're on Linux, swap ",[22,93851,93825],{},[22,93853,93854],{},"paplay",[22,93856,93857],{},"aplay"," with your system's notification sound.",[104,93860,93862],{"id":93861},"give-it-a-morning-greeting-sessionstart-hook","Give It a Morning Greeting (SessionStart Hook)",[27,93864,93865,93866,93868,93869,1017],{},"The completion sound already introduced you to hooks - shell commands that fire at specific points in Claude Code's lifecycle. But ",[22,93867,93821],{}," is just one of many lifecycle events. The one that really completes the sarcastic senior dev personality is ",[22,93870,93871],{},"SessionStart",[27,93873,93874,93876],{},[22,93875,93871],{}," fires every time you open a new Claude Code session. Its stdout is passed to Claude as context - meaning Claude sees the greeting and can act on it. By default, nothing happens - you just get a blinking cursor. But what if your Claude Code greeted you like a grumpy colleague who got to the office before you?",[27,93878,93879,93880,93882],{},"You can set this up by creating a small script and wiring it to the ",[22,93881,93871],{}," hook. Tell Claude Code:",[128,93884,93887],{"className":93885,"code":93886,"language":5189},[5187],"Create a SessionStart hook script at ~\u002F.claude\u002Fhooks\u002Fgreet.sh that picks a random sarcastic developer greeting and prints it to stdout. Use these greetings: \"Oh good, you're here. The codebase missed you. (It didn't.)\", \"Welcome back. Your TODO list hasn't gotten any shorter.\", \"Another day, another mass-deletion of node_modules.\", \"Ah, you again. Let's see what we break today.\", \"Good morning. The tests are still failing from yesterday.\", \"Back so soon? The tech debt isn't going anywhere.\". Make it executable and add it as a SessionStart hook in my user settings.\n",[22,93888,93886],{"__ignoreMap":133},[27,93890,93891,93892,894],{},"Under the hood, Claude will create a bash script that picks a random line and prints it, then wire it up in your ",[22,93893,93445],{},[128,93895,93897],{"className":5155,"code":93896,"language":5157,"meta":133,"style":133},"{\n    \"hooks\": {\n        \"SessionStart\": [\n            {\n                \"hooks\": [\n                    {\n                        \"type\": \"command\",\n                        \"command\": \"bash ~\u002F.claude\u002Fhooks\u002Fgreet.sh\"\n                    }\n                ]\n            }\n        ]\n    }\n}\n",[22,93898,93899,93903,93909,93916,93920,93926,93930,93940,93949,93953,93957,93961,93965,93969],{"__ignoreMap":133},[137,93900,93901],{"class":139,"line":140},[137,93902,15971],{"class":157},[137,93904,93905,93907],{"class":139,"line":173},[137,93906,93733],{"class":364},[137,93908,1819],{"class":157},[137,93910,93911,93914],{"class":139,"line":188},[137,93912,93913],{"class":364},"        \"SessionStart\"",[137,93915,93482],{"class":157},[137,93917,93918],{"class":139,"line":269},[137,93919,81549],{"class":157},[137,93921,93922,93924],{"class":139,"line":278},[137,93923,93763],{"class":364},[137,93925,93482],{"class":157},[137,93927,93928],{"class":139,"line":291},[137,93929,69183],{"class":157},[137,93931,93932,93934,93936,93938],{"class":139,"line":297},[137,93933,83944],{"class":364},[137,93935,726],{"class":157},[137,93937,93778],{"class":284},[137,93939,1961],{"class":157},[137,93941,93942,93944,93946],{"class":139,"line":302},[137,93943,93785],{"class":364},[137,93945,726],{"class":157},[137,93947,93948],{"class":284},"\"bash ~\u002F.claude\u002Fhooks\u002Fgreet.sh\"\n",[137,93950,93951],{"class":139,"line":662},[137,93952,63090],{"class":157},[137,93954,93955],{"class":139,"line":667},[137,93956,93799],{"class":157},[137,93958,93959],{"class":139,"line":786},[137,93960,760],{"class":157},[137,93962,93963],{"class":139,"line":798},[137,93964,93583],{"class":157},[137,93966,93967],{"class":139,"line":803},[137,93968,294],{"class":157},[137,93970,93971],{"class":139,"line":931},[137,93972,510],{"class":157},[27,93974,93975,93976,93979,93980,93982],{},"The script writes to ",[22,93977,93978],{},"stdout",", which ",[22,93981,93871],{}," passes to Claude as context. Claude sees the greeting but won't display it on its own - you need one more piece to make it visible.",[27,93984,93985],{},"This is where the output style comes in. By adding a rule to your custom output style (covered in the next section), you can instruct Claude to repeat the greeting verbatim as the first line of its response. The hook picks the random quip, and the output style ensures Claude actually says it out loud. Together, they give you a sarcastic morning greeting every time you start a session. It's like pair programming with someone who has opinions.",[27,93987,93988,93989,93991,93992,93995,93996,93999,94000,94003],{},"The beauty of hooks is that ",[22,93990,93871],{}," is just one of over a dozen lifecycle events. There's ",[22,93993,93994],{},"PreToolUse"," (before Claude runs a command), ",[22,93997,93998],{},"PostToolUse"," (after it edits a file), ",[22,94001,94002],{},"Notification"," (when Claude needs your attention), and more. You could go deep here - blocking dangerous commands, auto-formatting files, running tests after every edit. But for personality purposes, the session greeting is the sweet spot. It adds character without getting in the way of your actual work.",[104,94005,94007],{"id":94006},"give-it-an-attitude-custom-output-style","Give It an Attitude (Custom Output Style)",[27,94009,94010,94011,94014],{},"So far we've given Claude Code jokes, a dashboard, a voice, and a morning greeting. But what about how it actually ",[30,94012,94013],{},"talks"," to you? Output styles control how Claude formats its responses, and this is where you can really shape its personality.",[27,94016,94017,94018,94020,94021,94024],{},"You can pick from the built-in options by running ",[22,94019,31584],{}," → ",[22,94022,94023],{},"Output style"," or just tell Claude:",[128,94026,94029],{"className":94027,"code":94028,"language":5189},[5187],"Set my output style to Explanatory\n",[22,94030,94028],{"__ignoreMap":133},[27,94032,94033],{},"The three built-in options are:",[1003,94035,94036,94042,94048],{},[1006,94037,94038,94041],{},[42,94039,94040],{},"Default"," - the standard Claude Code experience, optimized for efficient software engineering",[1006,94043,94044,94047],{},[42,94045,94046],{},"Explanatory"," - adds educational \"Insights\" between tasks explaining implementation choices and codebase patterns",[1006,94049,94050,94053],{},[42,94051,94052],{},"Learning"," - collaborative learn-by-doing mode where Claude shares insights and also asks you to contribute small pieces of code yourself",[27,94055,94056,94057,94060],{},"But the real power comes from creating your own. Custom output styles are Markdown files with YAML frontmatter that live in ",[22,94058,94059],{},"~\u002F.claude\u002Foutput-styles\u002F",". This is where the personality really shines. You can tell Claude Code to create one:",[128,94062,94065],{"className":94063,"code":94064,"language":5189},[5187],"Create a custom output style file called \"senior-dev\" at ~\u002F.claude\u002Foutput-styles\u002Fsenior-dev.md. Make it sound like a blunt but helpful senior developer: no fluff, no hand-holding, give me the fix and a one-liner explaining why. If I'm doing something dumb, call it out. Use code comments over paragraphs. End with what to do next. Add YAML frontmatter with the name \"Senior Dev\" and description \"No fluff, just fixes.\"\n",[22,94066,94064],{"__ignoreMap":133},[27,94068,94069,94070,94072],{},"This is also where you wire up the session greeting from the previous section. The ",[22,94071,93871],{}," hook passes the greeting to Claude as context, but Claude won't display it unless you tell it to. Add a rule to your output style like:",[128,94074,94077],{"className":94075,"code":94076,"language":5189},[5187],"At the start of a new session, if there is a SessionStart hook greeting in your context, repeat it verbatim as your first line before doing anything else.\n",[22,94078,94076],{"__ignoreMap":133},[27,94080,94081],{},"Now the two features work together: the hook picks a random sarcastic quip, and the output style ensures Claude repeats it as the first thing you see in every new session.",[27,94083,94084,94085,94087],{},"Once created, it shows up as an option in ",[22,94086,31584],{},". I switch between styles depending on what I'm doing - \"Senior Dev\" when I'm in the zone and just need straight answers, \"Explanatory\" when I'm exploring a library I've never used before.",[27,94089,94090],{},[63,94091],{"alt":94092,"src":94093},"Custom Output Style","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1773917321\u002Fblog\u002Fi-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer\u002FScreenshot_2026-03-17_at_10.59.57_pm_arykjl",[104,94095,94097],{"id":94096},"give-it-opinions-personalised-spinner-tips","Give It Opinions (Personalised Spinner Tips)",[27,94099,94100,94101,94104],{},"The final touch. While the spinner verbs are the action words (\"",[22,94102,94103],{},"Bikeshedding the variable names...","\"), Claude Code also shows tips - those helpful hints that appear while you wait. The default tips are genuinely useful. But if you're going full sarcastic senior dev, you can replace them with something that has a bit more... judgement.",[128,94106,94109],{"className":94107,"code":94108,"language":5189},[5187],"Replace my spinner tips with these: \"Tip: that `any` type isn't fooling anyone\", \"Tip: your commit history reads like a mystery novel with no ending\", \"Tip: console.log is not a debugging strategy. But it works.\", \"Tip: no one has ever regretted writing a README. Start today.\", \"Tip: the component you're about to write already exists in your codebase\", \"Tip: future you will not remember what that regex does\"\n",[22,94110,94108],{"__ignoreMap":133},[27,94112,94113,94114,94117,94118,1017],{},"Under the hood, this uses ",[22,94115,94116],{},"spinnerTipsOverride"," in your settings. You can also toggle tips on or off entirely with ",[22,94119,94120],{},"spinnerTipsEnabled",[27,94122,94123],{},"Just like spinner verbs, the tips list is static - Claude Code won't generate new ones on its own. But you can ask Claude to generate a themed list for you:",[128,94125,94128],{"className":94126,"code":94127,"language":5189},[5187],"Replace my spinner tips with 20 tips that sound like a Vue developer who's tired of explaining why Vue is better than React\n",[22,94129,94127],{"__ignoreMap":133},[27,94131,94132],{},"Claude will write them all. It's a small thing that makes the wait time more entertaining - and occasionally, uncomfortably accurate.",[104,94134,94136],{"id":94135},"give-it-expertise-custom-skills","Give It Expertise (Custom Skills)",[27,94138,94139,94140,94143,94144,94147],{},"Everything so far has been cosmetic or ambient - spinners, sounds, greetings, style. Skills are where the personality becomes ",[30,94141,94142],{},"functional",". A Skill is a folder with a ",[22,94145,94146],{},"SKILL.md"," file that teaches Claude Code how to handle a specific task. Claude loads it automatically when relevant, so you don't have to repeat yourself across sessions.",[27,94149,94150,94151,94154],{},"Think of Skills as onboarding docs for Claude. You write the instructions once, and every time you trigger the Skill - either by typing ",[22,94152,94153],{},"\u002Fskill-name"," or by just asking Claude something that matches the Skill's description - it follows your playbook.",[27,94156,94157,94158,94163,94164,1017],{},"The official ",[45,94159,94162],{"href":94160,"target":2716,"rel":94161},"https:\u002F\u002Fgithub.com\u002Fanthropics\u002Fskills",[2718,2719],"anthropics\u002Fskills"," repository has dozens of examples covering everything from document creation to art generation. But for our sarcastic senior dev setup, I wanted something more personal: a code review Skill that reviews my code ",[30,94165,94166],{},"in character",[27,94168,94169],{},"Here's how to create one. Tell Claude Code:",[128,94171,94174],{"className":94172,"code":94173,"language":5189},[5187],"Create a personal skill at ~\u002F.claude\u002Fskills\u002Froast-my-code\u002FSKILL.md. It should be a code review skill called \"roast-my-code\" with the description: \"Reviews code like a sarcastic but helpful senior developer. Use when the user asks for a code review, says 'review this', 'roast my code', or 'what do you think of this'.\"\n\nThe skill instructions should tell Claude to:\n1. Start with an overall impression in one blunt sentence\n2. Point out anything genuinely wrong first (real issues, not nitpicks)\n3. Call out at least one thing that's actually good (even sarcastic seniors give credit)\n4. End with one concrete suggestion that would make the biggest difference\n5. Keep the tone dry and direct - think helpful colleague who's seen too much, not mean-spirited\n6. Use inline code comments style over long paragraphs\n7. Never be cruel, always be honest\n",[22,94175,94173],{"__ignoreMap":133},[27,94177,94178,94179,94181],{},"Claude Code will create the Skill directory and ",[22,94180,94146],{}," for you. The folder structure looks like this:",[128,94183,94186],{"className":94184,"code":94185,"language":5189},[5187],"~\u002F.claude\u002Fskills\u002F\n  └── roast-my-code\u002F\n      └── SKILL.md\n",[22,94187,94185],{"__ignoreMap":133},[27,94189,94190,94191,94194,94195,94197],{},"Once created, you can trigger it explicitly with ",[22,94192,94193],{},"\u002Froast-my-code"," or just say \"review this code\" and Claude will pick it up from the description. The key is that the Skill's ",[22,94196,88555],{}," field is what Claude uses to decide when to load it - so making it specific about trigger phrases helps it activate reliably.",[27,94199,94200],{},[63,94201],{"alt":94202,"src":94203},"Custom Skills","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1773917309\u002Fblog\u002Fi-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer\u002FScreenshot_2026-03-18_at_9.39.15_pm_lku1eu",[27,94205,94206],{},"Here's what makes Skills powerful: they use a progressive loading approach. Claude only reads the Skill's name and description at startup - the actual instructions don't enter the context window until the Skill is triggered. This means you can have multiple Skills installed without wasting context.",[27,94208,94209,94210,94213,94214,94216,94217,94220],{},"You can go further than a single Markdown file too. Skills can include reference files, templates, and even executable scripts. For example, you could add a ",[22,94211,94212],{},"STYLE_GUIDE.md"," inside the Skill folder with your team's actual coding conventions, and reference it from ",[22,94215,94146],{}," so Claude loads it when reviewing. Or add a ",[22,94218,94219],{},"scripts\u002Flint-check.sh"," that Claude runs as part of the review.",[27,94222,4737,94223,94228,94229,94233],{},[45,94224,94227],{"href":94225,"target":2716,"rel":94226},"https:\u002F\u002Fgithub.com\u002Fanthropics\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fskill-creator\u002FSKILL.md",[2718,2719],"Skill creator"," built into the official repository can also help you build and test new Skills interactively. You can browse the full collection at ",[45,94230,94232],{"href":94160,"target":2716,"rel":94231},[2718,2719],"github.com\u002Fanthropics\u002Fskills"," for inspiration - or just tell Claude Code what you need and let it create the Skill for you.",[104,94235,59075],{"id":59074},[27,94237,94238],{},"What started as \"let me just change the spinner text\" turned into a full personality makeover. My Claude Code now questions my TypeScript decisions while it thinks, greets me with a sarcastic one-liner every morning, shows me exactly how much context I've burned through, pings me with a sound when it's done, talks to me like a blunt senior dev, reviews my code in character, and drops passive-aggressive coding wisdom between responses. It's basically the most opinionated colleague I've ever had - and I configured every bit of it.",[27,94240,94241],{},"None of these customisations take more than a minute to set up, and that's what makes them great. You don't need to manually edit JSON files or dig through documentation. You just tell Claude Code what you want in plain English and it configures itself.",[27,94243,94244],{},"Here's a quick summary of everything we covered:",[1003,94246,94247,94253,94259,94265,94271,94277,94283],{},[1006,94248,94249,94252],{},[42,94250,94251],{},"Custom spinner verbs"," - give Claude a sense of humour while it thinks",[1006,94254,94255,94258],{},[42,94256,94257],{},"Live status line"," - a dashboard showing model, tokens, and session duration",[1006,94260,94261,94264],{},[42,94262,94263],{},"Completion sounds"," - get pinged when Claude finishes so you can multitask",[1006,94266,94267,94270],{},[42,94268,94269],{},"Session greeting"," - a sarcastic welcome message every time you start a session",[1006,94272,94273,94276],{},[42,94274,94275],{},"Custom output styles"," - shape how Claude talks to you",[1006,94278,94279,94282],{},[42,94280,94281],{},"Personalised tips"," - replace the default hints with unsolicited senior dev advice",[1006,94284,94285,94287],{},[42,94286,94202],{}," - teach Claude how to do specific tasks your way (like reviewing code in character)",[27,94289,94290,94291,94295,94296,94299],{},"Give them a try. All the configuration files, scripts, and skills from this article are available in the ",[45,94292,94294],{"href":93396,"target":2716,"rel":94293},[2718,2719],"companion GitHub repository",". And if you come up with better spinner verbs than \"",[22,94297,94298],{},"Blaming the previous developer (it was me)","\" - I'd love to hear them.",[2617,94301,94302],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":133,"searchDepth":173,"depth":173,"links":94304},[94305,94306,94307,94308,94309,94310,94311,94312],{"id":93403,"depth":173,"text":93404},{"id":93627,"depth":173,"text":93628},{"id":93697,"depth":173,"text":93698},{"id":93861,"depth":173,"text":93862},{"id":94006,"depth":173,"text":94007},{"id":94096,"depth":173,"text":94097},{"id":94135,"depth":173,"text":94136},{"id":59074,"depth":173,"text":59075},"Discover how to customise Claude Code's personality with custom spinner verbs, live status lines, completion sounds, session greetings, output styles, personalised tips, and custom skills. Turn your AI coding assistant into a sarcastic but helpful senior developer.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1773917254\u002Fblog\u002Fi-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer\u002Fi-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer_jpghb5",[94316,94317,94318,94319,94320,94321,94322,94323,94324,94325,94326,94327,94328,94329,59126,94330,94331,94332,94333,94334],"Claude Code","Claude Code customisation","Claude Code personality","sarcastic developer","spinner verbs","status line","Claude Code hooks","SessionStart hook","output style","Claude Code skills","custom skills","code review","AI coding assistant","terminal customisation","Claude Code settings","roast my code","productivity","developer experience","AI personality",{},"\u002F2026\u002F03\u002F22\u002Fi-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer","22nd March 2026",{"title":93332,"description":94313},"2026\u002F03\u002F22\u002Fi-gave-my-claude-code-the-personality-of-a-sarcastic-senior-developer","6gpNfgaQVsMVHj9a4W1Bln9xffvkQcHkK0r5IF-Jp0w",{"id":94342,"title":94343,"articleTags":94344,"author":11,"blog":12,"body":94345,"description":95025,"extension":2649,"image":95026,"keywords":95027,"meta":95046,"navigation":515,"path":95047,"published":95048,"readTime":798,"seo":95049,"stem":95050,"type":2662,"__hash__":95051},"content\u002F2026\u002F04\u002F05\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went.md","I Tried Redesigning My Blog with Google Stitch - Here's How It Actually Went",[27886,21755,10],{"type":14,"value":94346,"toc":95007},[94347,94350,94364,94366,94370,94375,94398,94407,94410,94414,94425,94430,94437,94440,94444,94456,94461,94480,94486,94490,94493,94499,94502,94511,94517,94520,94526,94532,94535,94544,94548,94551,94557,94563,94566,94569,94574,94578,94585,94591,94598,94604,94610,94614,94621,94626,94629,94635,94642,94648,94652,94663,94669,94681,94704,94710,94713,94717,94724,94730,94736,94743,94749,94752,94756,94759,94762,94825,94831,94837,94841,94844,94850,94856,94859,94863,94880,94885,94891,94896,94900,94940,94942,94980,94984,94989,94992,94998,95004],[17,94348,94343],{"id":94349},"i-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went",[27,94351,94352],{},[30,94353,94354,36,94356,40,94358],{},[33,94355],{"value":35},[33,94357],{"value":39},[42,94359,94360],{},[45,94361,94362],{"href":47},[33,94363],{"value":50},[52,94365],{":tags":54},[56,94367],{":audio-src":94368,":transcript-src":94369},"https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F04\u002F05\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fsummary.mp3","https:\u002F\u002Fcdn.jsdelivr.net\u002Fgh\u002FSuv4o\u002Fpersonal-blog-2023\u002Faudio-summary\u002F2026\u002F04\u002F05\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fsummary.json",[27,94371,94372],{},[63,94373],{"alt":12847,"src":94374},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775344739\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fcover-3_ufwobo",[27,94376,94377,94378,94383,94384,94389,94390,94393,94394,94397],{},"Google first launched Stitch at ",[45,94379,94382],{"href":94380,"target":2716,"rel":94381},"https:\u002F\u002Fdevelopers.googleblog.com\u002Fstitch-a-new-way-to-design-uis\u002F",[2718,2719],"I\u002FO 2025"," as an AI experiment - you'd type a prompt or upload a sketch, and it would generate UI designs and front-end code using Gemini. It was interesting but limited. Then in March 2026, they shipped a ",[45,94385,94388],{"href":94386,"target":2716,"rel":94387},"https:\u002F\u002Fblog.google\u002Finnovation-and-ai\u002Fmodels-and-research\u002Fgoogle-labs\u002Fstitch-ai-ui-design\u002F",[2718,2719],"major update"," that turned it into something much more ambitious: an infinite AI canvas with a design agent, interactive prototyping, voice commands, a ",[22,94391,94392],{},"DESIGN.md"," export for design systems, and an MCP server for plugging into developer tools. Google calls the new approach ",[42,94395,94396],{},"\"vibe design\""," - you describe the mood and objectives you want, and the AI handles the visual decisions.",[27,94399,94400,94401,94406],{},"My blog at ",[45,94402,94405],{"href":94403,"target":2716,"rel":94404},"https:\u002F\u002Fwww.trpkovski.com\u002F",[2718,2719],"trpkovski.com"," has had the same design for five years, and I've long run out of ideas for how to refresh it. The timing felt right to put Stitch's new capabilities to the test on a real project - not a hypothetical landing page, but my actual multi-page personal site.",[27,94408,94409],{},"What followed was a full day of experiments. Some clear failures, one genuine breakthrough, and a hard lesson about the gap between AI-generated design and production-ready code. This is Part 1 - the design journey. In Part 2, we'll take what Stitch produced and build it into a real site with Claude Code. Stay tuned for that.",[104,94411,94413],{"id":94412},"what-i-was-working-with","What I Was Working With",[27,94415,94400,94416,94419,94420,94424],{},[45,94417,94405],{"href":94403,"target":2716,"rel":94418},[2718,2719]," is built with Nuxt 3 and Tailwind CSS (",[45,94421,94423],{"href":47632,"target":2716,"rel":94422},[2718,2719],"source on GitHub","). It has seven sections: Home, Articles, Article Detail, The Keyboard Lab (my mechanical keyboard collection), Through The Lens (photography portfolio), About Me, and Get In Touch. Not a simple landing page - a real, multi-section personal site with a colour system of 12+ main colours and content ranging from code-heavy tutorials to photography galleries.",[27,94426,94427],{},[63,94428],{"alt":94413,"src":94429},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775346361\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fwhat-i-was-working-with_gqt9u9",[27,94431,94432,94433,94436],{},"The current design was created in ",[42,94434,94435],{},"Adobe XD"," five years ago. It's served me well, but a lot has changed since then - not just in design trends, but in the tools available. Adobe XD itself has been discontinued, Figma has taken over the design world, and now AI-powered tools like Stitch are promising to reshape the workflow entirely. It felt like the right time to see if the way we design websites has genuinely moved forward, or if AI design tools are still more hype than substance.",[27,94438,94439],{},"I wanted to see if Stitch could handle the complexity of a real multi-section site, or whether it's only suited to simpler, single-page projects.",[104,94441,94443],{"id":94442},"google-stitch-in-30-seconds","Google Stitch in 30 Seconds",[27,94445,94446,94447,94452,94453,94455],{},"Available for free at ",[45,94448,94451],{"href":94449,"target":2716,"rel":94450},"https:\u002F\u002Fstitch.withgoogle.com\u002F",[2718,2719],"stitch.withgoogle.com",". You feed it text prompts, screenshots, or sketches. It gives you back UI designs, interactive prototypes, HTML\u002FCSS code with Tailwind classes, and a ",[22,94454,94392],{}," design system file. You can also export to Figma or Google AI Studio.",[27,94457,94458],{},[63,94459],{"alt":94443,"src":94460},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343494\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fgoogle-stitch-in-30-seconds-2_nlkzsj",[27,94462,94463,94464,94467,94468,94471,94472,94475,94476,94479],{},"It has four generation modes: ",[42,94465,94466],{},"Gemini 3.0 Flash"," (optimised for speed), ",[42,94469,94470],{},"Gemini 3.1 Pro Thinking"," (optimised for quality and reasoning), ",[42,94473,94474],{},"Redesign"," (redesign existing apps from screenshots), and ",[42,94477,94478],{},"Ideate"," (explore creative directions through conversation before generating anything). Free while in Google Labs, with generation limits - 350 per month in Standard mode and 50 in Experimental mode.",[27,94481,94482],{},[63,94483],{"alt":94484,"src":94485},"Google Stitch Modes","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343511\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fgoogle-stitch-in-30-seconds-2_mv0sl0",[104,94487,94489],{"id":94488},"attempt-1-detailed-prompt-with-gemini-31-pro","Attempt 1: Detailed Prompt with Gemini 3.1 Pro",[27,94491,94492],{},"My first instinct was to be comprehensive. I wrote a ~5,000-character redesign brief with my full colour palette, every page URL on my site, detailed design direction, typography preferences, and page-by-page notes describing what each section should feel like. I used the most capable model available - Gemini 3.1 Pro Thinking.",[27,94494,94495],{},[63,94496],{"alt":94497,"src":94498},"Attempt 1 Prompt","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343494\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fattempt-1-give-it-everything_k4jq7t",[27,94500,94501],{},"To be fair, the result wasn't bad. Stitch understood the structure I was going for - hero section with an image and description, three article cards below, a \"view all posts\" button, subscription form, and a navigation menu. It was a pretty good job given a single detailed prompt.",[27,94503,94504,94505,94507,94508,94510],{},"Along with every generated design, Stitch produces a visual design system overview - a grid showing the colour palette with swatches and hex values, typography samples for headlines, body text, and labels, button variants, icon styles, and UI components. It's essentially a visual representation of what gets exported as the ",[22,94506,94392],{}," file - though you can't download the images or export this view to any format other than the ",[22,94509,94392],{}," itself.",[27,94512,94513],{},[63,94514],{"alt":94515,"src":94516},"DESIGN.md Overview","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343494\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002FDESIGN_MD_ubrahr",[27,94518,94519],{},"The layout followed my existing site's approach closely, which meant the output felt familiar rather than fresh. But the details had issues. The three article cards weren't the same height - a classic alignment problem. The hero image was AI-generated (a person standing in a sci-fi corridor) and had nothing to do with me. There was an unnecessary footer section with generic quick links. And Stitch hallucinated my location as \"Berlin, Germany\" - I've never lived in Berlin 🙂",[27,94521,94522],{},[63,94523],{"alt":94524,"src":94525},"Attempt 1 Result 1","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343493\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fattempt-1-give-it-everything-2_sckprg",[27,94527,94528],{},[63,94529],{"alt":94530,"src":94531},"Attempt 1 Result 2","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343493\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fattempt-1-give-it-everything-3_rw9fvq",[27,94533,94534],{},"Nothing catastrophic, but nothing that made me think \"this is better than what I already have.\" It was roughly the same design back at me, with a few rough edges and hallucinated details added.",[3244,94536,94537],{},[27,94538,94539,94540,94543],{},"A decent starting point from a single prompt, but I wanted something that would actually ",[30,94541,94542],{},"reimagine"," the layout, not just reproduce it with imperfections.",[104,94545,94547],{"id":94546},"attempt-2-short-prompt-with-gemini-30-flash","Attempt 2: Short Prompt with Gemini 3.0 Flash",[27,94549,94550],{},"For the second attempt, I stripped everything back to ~600 characters. No URLs, no page structure, no tech stack. Just the feeling I wanted. And this time I used the faster model - Gemini 3.0 Flash - to see if a simpler approach would produce cleaner results:",[128,94552,94555],{"className":94553,"code":94554,"language":5189},[5187],"A personal developer blog home page for a software engineer named Aleks. Dark mode first.\n\nVibe: playful, creative, developer-flavoured — like a terminal meets a modern magazine.\nNot corporate, not sterile.\n\nHero section with a terminal-style greeting like \"> hello, I'm Aleks_\" with a blinking\ncursor. Short bio underneath.\n\nBelow that, a \"Latest Articles\" section showing 3 blog post cards with colourful tag badges.\n\nFooter with a subscribe-to-newsletter form.\n\nColour palette: coral red (#ee5f53) as primary accent, dark navy (#173353) as base,\nlight coral (#f1918b) for secondary highlights.\n",[22,94556,94554],{"__ignoreMap":133},[27,94558,94559],{},[63,94560],{"alt":94561,"src":94562},"Attempt 2 Prompt","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343494\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fattempt-2-short-prompt-with-gemini-3.0-flash_rckdby",[27,94564,94565],{},"The difference was noticeable. All three article cards were the same height - no alignment bugs. The footer was minimal and intentional, with a single row of links. There was no AI-generated images this time - just clean cards with coloured gradient top borders that looked deliberate rather than placeholder. The subscribe section was tight and purposeful: \"Stay Synchronized_\" heading with an inline email input.",[27,94567,94568],{},"Compared to Attempt 1, the shorter prompt with the faster model produced a cleaner, more consistent design. No hallucinated locations, no generic sci-fi hero images, no misaligned cards.",[3244,94570,94571],{},[27,94572,94573],{},"Counter-intuitive finding: the faster, simpler model produced a better design than the thinking\u002Freasoning model. Less overthinking meant fewer layout bugs. If you're using Stitch, don't assume the most powerful model gives the best results.",[104,94575,94577],{"id":94576},"attempt-3-redesign-my-actual-site","Attempt 3: Redesign My Actual Site",[27,94579,94580,94581,94584],{},"Stitch has a ",[42,94582,94583],{},"Redesign mode"," that takes a screenshot of an existing app and redesigns it. This felt like the most natural approach for my situation - show Stitch what I already have and let it reimagine it. I took a screenshot of my current home page and fed it in.",[27,94586,94587],{},[63,94588],{"alt":94589,"src":94590},"Attempt 3 Screenshot","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343494\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fattempt-3-redesign-my-actual-site_kco5ly",[27,94592,94593,94594,94597],{},"This was arguably the worst attempt of all. Stitch followed my existing layout ",[30,94595,94596],{},"too closely"," - same single-column structure, same content flow, same vertical rhythm - and just wrapped everything in clunky terminal-style window frames. My clean, readable site became cramped and busy. It wasn't a redesign - it was a reskin, and not a flattering one.",[27,94599,94600,94601,94603],{},"On top of that, the OCR quality was rough. Because Redesign mode reads content from the screenshot, article descriptions came back garbled - \"JevaEcrtge!\" instead of \"JavaScript\" and \"Tipkevki\" instead of \"Trpkovski.\" So the design was a step backwards ",[30,94602,71355],{}," it couldn't even reproduce my content accurately.",[27,94605,94606],{},[63,94607],{"alt":94608,"src":94609},"Attempt 3 Result","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343494\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fattempt-3-redesign-my-actual-site-2_hsddeh",[104,94611,94613],{"id":94612},"attempt-4-ideate-mode-everything-changed","Attempt 4: Ideate Mode - Everything Changed",[27,94615,94616,94617,94620],{},"This is where the story turns. Stitch's ",[42,94618,94619],{},"Ideate mode"," works fundamentally differently from the other modes. It doesn't jump straight to generating visuals. Instead, it thinks first - proposing creative directions, building a specification, asking you to make choices before it touches a single pixel.",[3244,94622,94623],{},[27,94624,94625],{},"Ideate mode is only available when starting a new project. You can't switch to it when iterating on existing designs.",[27,94627,94628],{},"I started a fresh project and described my vision.",[27,94630,94631],{},[63,94632],{"alt":94633,"src":94634},"Ideate Mode Start","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343494\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fattempt-4-ideate-mode-everything-changed_ze9vvx",[27,94636,94637,94638,94641],{},"Instead of generating a design, Stitch came back with ",[42,94639,94640],{},"three distinct creative directions",": Neon Terminal (dark, immersive, CRT glows and aggressive asymmetry), Soft Cyber (bright, airy, floating elements in whitespace), and Brutalist Arcade (raw thick borders, 8-bit aesthetic, editorial zine feel).",[27,94643,94644,94645,1017],{},"For the first time in this experiment, I was making a creative decision instead of just accepting whatever the AI generated. That shift - from passive recipient to active collaborator - made all the difference. I chose ",[42,94646,94647],{},"Neon Terminal",[123,94649,94651],{"id":94650},"the-prd-this-is-the-game-changer","The PRD - This Is the Game Changer",[27,94653,94654,94655,94658,94659,94662],{},"Before generating any visuals, Stitch produced a detailed ",[42,94656,94657],{},"Product Requirements Document"," - a proper written specification for the entire project. This is what makes Ideate mode fundamentally different. Other modes jump straight to pixels. A PRD captures the ",[30,94660,94661],{},"thinking"," behind the design - why it works, how interactions behave, what happens in edge cases - which is infinitely more useful when you hand it to a developer or coding agent later.",[27,94664,94665],{},[63,94666],{"alt":94667,"src":94668},"PRD Part 1","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343495\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fthe-prd-this-is-the-game-changer-1_wt2vff",[27,94670,94671,94672,164,94675,164,94678,4409],{},"The PRD included a complete design system with CSS custom properties, four screen specifications (Home, Article Detail, Archive, and a bonus \"Now\" page) each with layouts, components, and responsive breakpoints, user flows with step-by-step interaction descriptions, and screen states for empty\u002Floading\u002Ferror conditions - all themed to the terminal aesthetic (",[22,94673,94674],{},"NO LOGS FOUND",[22,94676,94677],{},"[ SYSTEM BOOTING... ]",[22,94679,94680],{},"ERR_CONNECTION_REFUSED",[27,94682,94683,94684,94687,94688,94691,94692,94695,94696,94699,94700,94703],{},"But my favourite part was the UX copy. ",[22,94685,94686],{},"> whoami"," as the hero heading. ",[22,94689,94690],{},"> .\u002Fsubscribe.sh"," for the newsletter. ",[22,94693,94694],{},"EXEC_TIME: 4ms"," instead of \"4 min read.\" ",[22,94697,94698],{},"> READ.EXE"," instead of \"Read more.\" Tag badges as ",[22,94701,94702],{},"[#TypeScript]",". These creative details would take hours to brainstorm manually - Stitch generated them as part of a coherent design language in seconds.",[27,94705,94706],{},[63,94707],{"alt":94708,"src":94709},"PRD Part 2","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343495\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fthe-prd-this-is-the-game-changer-2_pkrjzc",[27,94711,94712],{},"The PRD was more valuable than all the visual outputs from my first three attempts combined.",[123,94714,94716],{"id":94715},"the-screens","The Screens",[27,94718,94719,94720,94723],{},"After approving the PRD, Stitch generated the actual UI screens. Slower than Flash, but noticeably better quality. It also generates ",[42,94721,94722],{},"desktop, tablet, and mobile views"," automatically - you can preview responsive behaviour before writing a single line of CSS.",[27,94725,94726],{},[63,94727],{"alt":94728,"src":94729},"Screens Part 1","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343496\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fthe-screens-1_qnqrkx",[27,94731,94732],{},[63,94733],{"alt":94734,"src":94735},"Screens Part 2","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343496\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fthe-screens-2_bxztzn",[27,94737,94738,94739,94742],{},"And there's a ",[42,94740,94741],{},"\"Copy as code\""," feature: right-click any screen and get the complete HTML\u002FCSS\u002FJavaScript with Tailwind config and animations included.",[27,94744,94745],{},[63,94746],{"alt":94747,"src":94748},"Copy as Code","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343495\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fthe-screens-3.jpg_n1o1oz",[27,94750,94751],{},"Though it wasn't perfect. Overflow issues appeared across multiple tablet and mobile views - on the Home page, for instance, the navigation Subscribe button was cut off and overflowing on mobile. The Article Detail tablet view had similar problems with content spilling outside its container. And one screen got permanently stuck in \"Generating Screen...\" mode and never completed. The tool is still experimental, and the responsive behaviour needs work.",[104,94753,94755],{"id":94754},"refining-and-building-out-all-pages","Refining and Building Out All Pages",[27,94757,94758],{},"The Ideate output was the best by far, but it was generic - \"Neon Terminal\" as the site name, placeholder navigation, wrong fonts. I refined it with my actual branding (site name, navigation structure, Ubuntu font, tag colours, social links) and then generated the remaining pages one at a time with Gemini 3.0 Flash.",[27,94760,94761],{},"Each page brought its own creative surprises:",[1003,94763,94764,94780,94793,94803],{},[1006,94765,94766,94769,94770,2214,94773,94776,94777,94779],{},[42,94767,94768],{},"Keyboard Lab"," - grayscale-to-colour hover effects on keyboard photos, specs as terminal readouts (",[22,94771,94772],{},"> Switch: [Topre 45g Silent]",[22,94774,94775],{},"> INSPECT.EXE"," buttons matching the ",[22,94778,94698],{}," pattern",[1006,94781,94782,94785,94786,94789,94790],{},[42,94783,94784],{},"Through The Lens"," - portrait-ratio gallery cards with backdrop-blur category labels, photo count badges (",[22,94787,94788],{},"[ 12 PHOTOS ]","), terminal headers like ",[22,94791,94792],{},"CAT_01_ASTRO.EXE",[1006,94794,94795,94798,94799,94802],{},[42,94796,94797],{},"About Me"," - career timeline styled as ",[22,94800,94801],{},"GIT_LOG --CAREER --ONELINE"," with actual commit hashes and fading opacity for older roles. My favourite creative touch from the entire experiment",[1006,94804,94805,94808,94809,164,94812,164,94815,94818,94819,164,94822],{},[42,94806,94807],{},"Get In Touch"," - form labels as ",[22,94810,94811],{},"IDENTIFY_USER",[22,94813,94814],{},"RETURN_PATH",[22,94816,94817],{},"TRANSMISSION_DATA",". Social links as terminal commands: ",[22,94820,94821],{},"> ssh github.com\u002Ftrpkovski",[22,94823,94824],{},"> ping linkedin.com\u002Fin\u002Ftrpkovski",[27,94826,94827],{},[63,94828],{"alt":94829,"src":94830},"Refining Pages 1","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343511\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Frefining-and-building-out-all-pages-1_snnmzx",[27,94832,94833],{},[63,94834],{"alt":94835,"src":94836},"Refining Pages 2","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343511\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Frefining-and-building-out-all-pages-2_efbpdu",[104,94838,94840],{"id":94839},"the-big-problem-consistency","The Big Problem: Consistency",[27,94842,94843],{},"Each page looked good in isolation. Together, they were a mess of inconsistencies.",[27,94845,94846,94849],{},[42,94847,94848],{},"Stitch does not maintain a design system across screens."," Every new page generated its own Tailwind config with different colour tokens, different font imports, and different structural patterns. I asked for Ubuntu as the body font repeatedly and kept getting Plus Jakarta Sans or Space Grotesk. The site name appeared as \"NEON_TERMINAL\" on one page, \"NEON.SH\" on another, and \"trpkovski.com\" on a third. Footer layouts were different on every single screen. The CRT scanline effect - a core part of the aesthetic - was implemented multiple entirely different ways across eight pages.",[27,94851,94852],{},[63,94853],{"alt":94854,"src":94855},"Consistency Issues","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343511\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fthe-big-problem-consistency_o0o3um",[27,94857,94858],{},"This is the most important finding from the entire experiment: Stitch gives you creative direction, not a consistent codebase. You will need to unify everything yourself.",[104,94860,94862],{"id":94861},"the-export","The Export",[27,94864,94865,94866,94869,94870,30455,94873,94876,94877,94879],{},"Select all screens, click ",[42,94867,94868],{},"Export → ZIP",", and you get a clean package: each screen as ",[22,94871,94872],{},"code.html",[22,94874,94875],{},"screen.png",", plus a shared ",[22,94878,94392],{}," capturing the design system. You can also export to Figma (Standard Mode) or Google AI Studio.",[27,94881,94882],{},[63,94883],{"alt":94862,"src":94884},"https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_750,e_sharpen:100\u002Fv1775343491\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fthe-export_blawjz",[128,94886,94889],{"className":94887,"code":94888,"language":5189},[5187],"stitch_neon_terminal_prd\u002F\n├── neon_terminal\u002F\n│   └── DESIGN.md\n├── home\u002F\n│   ├── code.html\n│   └── screen.png\n├── article_detail\u002F\n│   ├── code.html\n│   └── screen.png\n├── the_keyboard_lab\u002F\n│   ├── code.html\n│   └── screen.png\n├── through_the_lens\u002F\n│   ├── code.html\n│   └── screen.png\n├── about_me\u002F\n│   ├── code.html\n│   └── screen.png\n└── get_in_touch\u002F\n    ├── code.html\n    └── screen.png\n",[22,94890,94888],{"__ignoreMap":133},[27,94892,4737,94893,94895],{},[22,94894,94392],{}," captures the creative philosophy, colour palette with surface hierarchy, typography pairings, component patterns, and explicit do's and don'ts. Useful as a starting point, though it inherits the same inconsistencies from the screens.",[104,94897,94899],{"id":94898},"whats-good","What's Good",[1003,94901,94902,94908,94914,94920,94926,94932],{},[1006,94903,94904,94907],{},[42,94905,94906],{},"Ideate mode is genuinely powerful."," It feels like working with a design consultant, not a random image generator. The multi-step workflow - concepts, then spec, then visuals - produces far better results than jumping straight to pixels",[1006,94909,94910,94913],{},[42,94911,94912],{},"The PRD is often more valuable than the visuals."," Design tokens, component specs, interaction states, UX copy - this is real design system thinking",[1006,94915,94916,94919],{},[42,94917,94918],{},"\"Copy as code\" bridges design and development."," Clean HTML\u002FCSS\u002FTailwind for any screen, ready to feed into a coding workflow",[1006,94921,94922,94925],{},[42,94923,94924],{},"Completely free"," while in Google Labs",[1006,94927,94928,94931],{},[42,94929,94930],{},"Creative ideas you wouldn't come up with"," - details that would take hours to brainstorm manually",[1006,94933,94934,94937,94938],{},[42,94935,94936],{},"Multiple export paths."," Figma, Google AI Studio, ZIP with code and assets, ",[22,94939,94392],{},[123,94941,73243],{"id":73242},[1003,94943,94944,94950,94956,94962,94968,94974],{},[1006,94945,94946,94949],{},[42,94947,94948],{},"No design system consistency across screens."," Every page generates its own config, colours, and fonts - the biggest limitation",[1006,94951,94952,94955],{},[42,94953,94954],{},"Ignores specific requests."," I asked for Ubuntu font repeatedly and kept getting other fonts",[1006,94957,94958,94961],{},[42,94959,94960],{},"Layout bugs persist."," Misaligned cards, overflow issues, tablet views that cut off content",[1006,94963,94964,94967],{},[42,94965,94966],{},"Redesign mode is essentially a reskin."," Too faithful to the original structure to be useful for genuine redesigns",[1006,94969,94970,94973],{},[42,94971,94972],{},"OCR errors and hallucinated content."," Garbled text from screenshots, invented locations, fabricated career histories",[1006,94975,94976,94979],{},[42,94977,94978],{},"Ideate mode locked to new projects."," The most powerful mode isn't available when iterating on existing work",[104,94981,94983],{"id":94982},"so-was-it-successful","So Was It Successful?",[27,94985,94986],{},[42,94987,94988],{},"About halfway there.",[27,94990,94991],{},"Stitch didn't give me anything close to a production-ready redesign. The designs have inconsistent colour systems, wrong fonts, varying layouts, and bugs that would need serious work before anyone could ship them. If I'm judging purely on \"can I take this output and deploy it\" - the answer is no.",[27,94993,94994,94995,94997],{},"But that's not really the right question. What Stitch ",[30,94996,82986],{}," give me was a design language I wouldn't have arrived at on my own. The terminal aesthetic, the interaction patterns, the UX copy, the component system (Hardware Header + Media Bay + Console Output) - these are genuinely good design decisions that came from a few conversations with an AI. The PRD from Ideate mode alone was worth the entire experiment.",[27,94999,95000,95001,95003],{},"The real question is whether the other half can be closed by pairing Stitch's output with a coding agent. Can Claude Code take the ",[22,95002,94392],{},", the exported screens, and a spec-driven workflow, and turn this 50% into something I'd actually want to deploy?",[27,95005,95006],{},"That's exactly what we'll find out in Part 2. The design is done. Let's see if the code can keep up.",{"title":133,"searchDepth":173,"depth":173,"links":95008},[95009,95010,95011,95012,95013,95014,95018,95019,95020,95021,95024],{"id":94412,"depth":173,"text":94413},{"id":94442,"depth":173,"text":94443},{"id":94488,"depth":173,"text":94489},{"id":94546,"depth":173,"text":94547},{"id":94576,"depth":173,"text":94577},{"id":94612,"depth":173,"text":94613,"children":95015},[95016,95017],{"id":94650,"depth":188,"text":94651},{"id":94715,"depth":188,"text":94716},{"id":94754,"depth":173,"text":94755},{"id":94839,"depth":173,"text":94840},{"id":94861,"depth":173,"text":94862},{"id":94898,"depth":173,"text":94899,"children":95022},[95023],{"id":73242,"depth":188,"text":73243},{"id":94982,"depth":173,"text":94983},"A deep dive into redesigning a personal developer blog using Google Stitch's AI-powered design tools. This hands-on experiment covers different approaches, from detailed prompts to Ideate mode, revealing what works and what doesn't when using AI for real web design projects.","https:\u002F\u002Fres.cloudinary.com\u002Fsuv4o\u002Fimage\u002Fupload\u002Fq_auto,f_auto,w_1200,e_sharpen:100\u002Fv1775344739\u002Fblog\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went\u002Fcover-3_ufwobo",[95028,95029,95030,95031,95032,95033,95034,95035,22769,21096,95036,95037,94619,95038,95039,95040,95041,95042,95043,95044,95045],"Google Stitch","AI design tools","UI design","web design","vibe design","Gemini","AI UI generator","design system","personal blog redesign","Google Labs","PRD","product requirements document","AI prototyping","front-end design","design to code","AI experiment","terminal aesthetic","neon terminal design",{},"\u002F2026\u002F04\u002F05\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went","5th April 2026",{"title":94343,"description":95025},"2026\u002F04\u002F05\u002Fi-tried-redesigning-my-blog-with-google-stitch-heres-how-it-actually-went","vAeb9PEDphQHHGsuyHt29MjNSzpYY4vEm4TR86K3e4U",[95053],{"id":94342,"title":94343,"articleTags":95054,"author":11,"blog":12,"body":95055,"description":95025,"extension":2649,"image":95026,"keywords":95501,"meta":95502,"navigation":515,"path":95047,"published":95048,"readTime":798,"seo":95503,"stem":95050,"type":2662,"__hash__":95051},[27886,21755,10],{"type":14,"value":95056,"toc":95483},[95057,95059,95073,95075,95077,95081,95093,95098,95100,95102,95110,95114,95118,95120,95122,95129,95133,95143,95147,95149,95151,95155,95157,95163,95167,95169,95173,95177,95179,95185,95187,95189,95194,95198,95200,95202,95206,95208,95212,95216,95220,95224,95228,95230,95234,95238,95240,95244,95248,95252,95254,95260,95264,95272,95284,95288,95290,95292,95296,95300,95304,95308,95312,95314,95316,95318,95320,95360,95364,95368,95370,95372,95376,95380,95382,95384,95394,95398,95403,95407,95409,95437,95439,95465,95467,95471,95473,95477,95481],[17,95058,94343],{"id":94349},[27,95060,95061],{},[30,95062,95063,36,95065,40,95067],{},[33,95064],{"value":35},[33,95066],{"value":39},[42,95068,95069],{},[45,95070,95071],{"href":47},[33,95072],{"value":50},[52,95074],{":tags":54},[56,95076],{":audio-src":94368,":transcript-src":94369},[27,95078,95079],{},[63,95080],{"alt":12847,"src":94374},[27,95082,94377,95083,94383,95086,94389,95089,94393,95091,94397],{},[45,95084,94382],{"href":94380,"target":2716,"rel":95085},[2718,2719],[45,95087,94388],{"href":94386,"target":2716,"rel":95088},[2718,2719],[22,95090,94392],{},[42,95092,94396],{},[27,95094,94400,95095,94406],{},[45,95096,94405],{"href":94403,"target":2716,"rel":95097},[2718,2719],[27,95099,94409],{},[104,95101,94413],{"id":94412},[27,95103,94400,95104,94419,95107,94424],{},[45,95105,94405],{"href":94403,"target":2716,"rel":95106},[2718,2719],[45,95108,94423],{"href":47632,"target":2716,"rel":95109},[2718,2719],[27,95111,95112],{},[63,95113],{"alt":94413,"src":94429},[27,95115,94432,95116,94436],{},[42,95117,94435],{},[27,95119,94439],{},[104,95121,94443],{"id":94442},[27,95123,94446,95124,94452,95127,94455],{},[45,95125,94451],{"href":94449,"target":2716,"rel":95126},[2718,2719],[22,95128,94392],{},[27,95130,95131],{},[63,95132],{"alt":94443,"src":94460},[27,95134,94463,95135,94467,95137,94471,95139,94475,95141,94479],{},[42,95136,94466],{},[42,95138,94470],{},[42,95140,94474],{},[42,95142,94478],{},[27,95144,95145],{},[63,95146],{"alt":94484,"src":94485},[104,95148,94489],{"id":94488},[27,95150,94492],{},[27,95152,95153],{},[63,95154],{"alt":94497,"src":94498},[27,95156,94501],{},[27,95158,94504,95159,94507,95161,94510],{},[22,95160,94392],{},[22,95162,94392],{},[27,95164,95165],{},[63,95166],{"alt":94515,"src":94516},[27,95168,94519],{},[27,95170,95171],{},[63,95172],{"alt":94524,"src":94525},[27,95174,95175],{},[63,95176],{"alt":94530,"src":94531},[27,95178,94534],{},[3244,95180,95181],{},[27,95182,94539,95183,94543],{},[30,95184,94542],{},[104,95186,94547],{"id":94546},[27,95188,94550],{},[128,95190,95192],{"className":95191,"code":94554,"language":5189},[5187],[22,95193,94554],{"__ignoreMap":133},[27,95195,95196],{},[63,95197],{"alt":94561,"src":94562},[27,95199,94565],{},[27,95201,94568],{},[3244,95203,95204],{},[27,95205,94573],{},[104,95207,94577],{"id":94576},[27,95209,94580,95210,94584],{},[42,95211,94583],{},[27,95213,95214],{},[63,95215],{"alt":94589,"src":94590},[27,95217,94593,95218,94597],{},[30,95219,94596],{},[27,95221,94600,95222,94603],{},[30,95223,71355],{},[27,95225,95226],{},[63,95227],{"alt":94608,"src":94609},[104,95229,94613],{"id":94612},[27,95231,94616,95232,94620],{},[42,95233,94619],{},[3244,95235,95236],{},[27,95237,94625],{},[27,95239,94628],{},[27,95241,95242],{},[63,95243],{"alt":94633,"src":94634},[27,95245,94637,95246,94641],{},[42,95247,94640],{},[27,95249,94644,95250,1017],{},[42,95251,94647],{},[123,95253,94651],{"id":94650},[27,95255,94654,95256,94658,95258,94662],{},[42,95257,94657],{},[30,95259,94661],{},[27,95261,95262],{},[63,95263],{"alt":94667,"src":94668},[27,95265,94671,95266,164,95268,164,95270,4409],{},[22,95267,94674],{},[22,95269,94677],{},[22,95271,94680],{},[27,95273,94683,95274,94687,95276,94691,95278,94695,95280,94699,95282,94703],{},[22,95275,94686],{},[22,95277,94690],{},[22,95279,94694],{},[22,95281,94698],{},[22,95283,94702],{},[27,95285,95286],{},[63,95287],{"alt":94708,"src":94709},[27,95289,94712],{},[123,95291,94716],{"id":94715},[27,95293,94719,95294,94723],{},[42,95295,94722],{},[27,95297,95298],{},[63,95299],{"alt":94728,"src":94729},[27,95301,95302],{},[63,95303],{"alt":94734,"src":94735},[27,95305,94738,95306,94742],{},[42,95307,94741],{},[27,95309,95310],{},[63,95311],{"alt":94747,"src":94748},[27,95313,94751],{},[104,95315,94755],{"id":94754},[27,95317,94758],{},[27,95319,94761],{},[1003,95321,95322,95332,95340,95346],{},[1006,95323,95324,94769,95326,2214,95328,94776,95330,94779],{},[42,95325,94768],{},[22,95327,94772],{},[22,95329,94775],{},[22,95331,94698],{},[1006,95333,95334,94785,95336,94789,95338],{},[42,95335,94784],{},[22,95337,94788],{},[22,95339,94792],{},[1006,95341,95342,94798,95344,94802],{},[42,95343,94797],{},[22,95345,94801],{},[1006,95347,95348,94808,95350,164,95352,164,95354,94818,95356,164,95358],{},[42,95349,94807],{},[22,95351,94811],{},[22,95353,94814],{},[22,95355,94817],{},[22,95357,94821],{},[22,95359,94824],{},[27,95361,95362],{},[63,95363],{"alt":94829,"src":94830},[27,95365,95366],{},[63,95367],{"alt":94835,"src":94836},[104,95369,94840],{"id":94839},[27,95371,94843],{},[27,95373,95374,94849],{},[42,95375,94848],{},[27,95377,95378],{},[63,95379],{"alt":94854,"src":94855},[27,95381,94858],{},[104,95383,94862],{"id":94861},[27,95385,94865,95386,94869,95388,30455,95390,94876,95392,94879],{},[42,95387,94868],{},[22,95389,94872],{},[22,95391,94875],{},[22,95393,94392],{},[27,95395,95396],{},[63,95397],{"alt":94862,"src":94884},[128,95399,95401],{"className":95400,"code":94888,"language":5189},[5187],[22,95402,94888],{"__ignoreMap":133},[27,95404,4737,95405,94895],{},[22,95406,94392],{},[104,95408,94899],{"id":94898},[1003,95410,95411,95415,95419,95423,95427,95431],{},[1006,95412,95413,94907],{},[42,95414,94906],{},[1006,95416,95417,94913],{},[42,95418,94912],{},[1006,95420,95421,94919],{},[42,95422,94918],{},[1006,95424,95425,94925],{},[42,95426,94924],{},[1006,95428,95429,94931],{},[42,95430,94930],{},[1006,95432,95433,94937,95435],{},[42,95434,94936],{},[22,95436,94392],{},[123,95438,73243],{"id":73242},[1003,95440,95441,95445,95449,95453,95457,95461],{},[1006,95442,95443,94949],{},[42,95444,94948],{},[1006,95446,95447,94955],{},[42,95448,94954],{},[1006,95450,95451,94961],{},[42,95452,94960],{},[1006,95454,95455,94967],{},[42,95456,94966],{},[1006,95458,95459,94973],{},[42,95460,94972],{},[1006,95462,95463,94979],{},[42,95464,94978],{},[104,95466,94983],{"id":94982},[27,95468,95469],{},[42,95470,94988],{},[27,95472,94991],{},[27,95474,94994,95475,94997],{},[30,95476,82986],{},[27,95478,95000,95479,95003],{},[22,95480,94392],{},[27,95482,95006],{},{"title":133,"searchDepth":173,"depth":173,"links":95484},[95485,95486,95487,95488,95489,95490,95494,95495,95496,95497,95500],{"id":94412,"depth":173,"text":94413},{"id":94442,"depth":173,"text":94443},{"id":94488,"depth":173,"text":94489},{"id":94546,"depth":173,"text":94547},{"id":94576,"depth":173,"text":94577},{"id":94612,"depth":173,"text":94613,"children":95491},[95492,95493],{"id":94650,"depth":188,"text":94651},{"id":94715,"depth":188,"text":94716},{"id":94754,"depth":173,"text":94755},{"id":94839,"depth":173,"text":94840},{"id":94861,"depth":173,"text":94862},{"id":94898,"depth":173,"text":94899,"children":95498},[95499],{"id":73242,"depth":188,"text":73243},{"id":94982,"depth":173,"text":94983},[95028,95029,95030,95031,95032,95033,95034,95035,22769,21096,95036,95037,94619,95038,95039,95040,95041,95042,95043,95044,95045],{},{"title":94343,"description":95025},1775349168887]