[{"data":1,"prerenderedAt":10962},["ShallowReactive",2],{"home-work":3,"home-writing":8921},[4,67,1194,2452,3949,4975,6156,7246,8429],{"id":5,"title":6,"body":7,"date":53,"description":54,"extension":55,"externalUrl":36,"featured":56,"kind":57,"meta":58,"navigation":56,"path":59,"seo":60,"stem":61,"tags":62,"__hash__":66},"work\u002Fwork\u002Fspotlight.md","Spotlight",{"type":8,"value":9,"toc":47},"minimark",[10,14,18,23,26,30],[11,12,6],"h1",{"id":13},"spotlight",[15,16,17],"p",{},"A Chrome extension I'm building as an indie product. Highlight what matters, dim the rest, and capture clean, focused screenshots in one click — for demos, docs, bug reports, and tutorials.",[19,20,22],"h2",{"id":21},"why-im-building-it","Why I'm building it",[15,24,25],{},"Every screenshot tool captures the whole screen and leaves the framing to you. Spotlight flips that: you point at what matters, everything else dims, and the capture comes out clean and focused — no cropping, annotating, or blurring afterwards.",[19,27,29],{"id":28},"status","Status",[15,31,32,33,40,41,46],{},"In active development. Live at ",[34,35,39],"a",{"href":36,"rel":37},"https:\u002F\u002Ftryspotlight.app",[38],"nofollow","tryspotlight.app",". Build notes will live on ",[34,42,45],{"href":43,"rel":44},"https:\u002F\u002Fbuildwithcheese.com",[38],"buildwithcheese.com",".",{"title":48,"searchDepth":49,"depth":49,"links":50},"",2,[51,52],{"id":21,"depth":49,"text":22},{"id":28,"depth":49,"text":29},"2026-01-15","A Chrome extension to highlight what matters, dim the rest, and capture clean screenshots in one click.","md",true,"build",{},"\u002Fwork\u002Fspotlight",{"title":6,"description":54},"work\u002Fspotlight",[63,64,65],"Chrome Extension","TypeScript","Indie","QiWlXUSKS1yfVkbHBlZCL9HcqeopimGeCgIUqUv_AWY",{"id":68,"title":69,"body":70,"date":1178,"description":1179,"extension":55,"externalUrl":1180,"featured":1181,"kind":1182,"meta":1183,"navigation":56,"path":1185,"seo":1186,"stem":1187,"tags":1188,"__hash__":1193},"work\u002Fwork\u002Fanonymous-link-generator.md","Bulk link generator: platform extension",{"type":8,"value":71,"toc":1165},[72,75,79,86,94,98,101,120,123,127,130,147,152,155,785,789,798,835,843,885,893,1006,1010,1013,1045,1049,1095,1099,1102,1113,1116,1120,1123,1126,1130,1133,1136,1150,1153,1156,1161],[11,73,69],{"id":74},"bulk-link-generator-platform-extension",[19,76,78],{"id":77},"context","Context",[15,80,81,82,46],{},"Sometimes the most satisfying solutions come from working around limitations rather than waiting for them to be solved. When our Customer Success team came to me with an urgent request, the situation was clear: A client needed to generate hundreds of anonymous form links, but our platform only supported creating these one at a time (via our UI). The proper feature was on the roadmap, but the client needed a solution ",[83,84,85],"em",{},"now",[87,88,89],"blockquote",{},[15,90,91],{},[83,92,93],{},"Note: While specific client details are kept confidential, this case study shows how creative automation can bridge the gap between immediate customer needs and planned feature development.",[19,95,97],{"id":96},"the-challenge","The Challenge",[15,99,100],{},"Picture this: You have a platform feature that lets you create a single anonymous form link. Great! But what if you need 173 of them? Not so great. The manual process would involve:",[102,103,104,108,111,114,117],"ol",{},[105,106,107],"li",{},"Start a new workflow",[105,109,110],{},"Wait for it to initialize",[105,112,113],{},"Find the anonymous link in the metadata",[105,115,116],{},"Copy it somewhere safe",[105,118,119],{},"Repeat... 172 more times 😱",[15,121,122],{},"Our CSM team's collective response: \"There has to be a better way!\"",[19,124,126],{"id":125},"what-i-built","What I Built",[15,128,129],{},"I developed what I like to call the \"Link-O-Matic 3000\" (okay, it's just a TypeScript script, but let's have some fun with it). The script could:",[102,131,132,135,138,141,144],{},[105,133,134],{},"Authenticate with our platform",[105,136,137],{},"Create workflows in rapid succession",[105,139,140],{},"Extract anonymous links from workflow metadata",[105,142,143],{},"Generate a nicely formatted CSV file",[105,145,146],{},"Do it all without human intervention",[148,149,151],"h3",{"id":150},"the-secret-sauce","The Secret Sauce",[15,153,154],{},"The magic happened in understanding the workflow creation process. Here's the core logic:",[156,157,161],"pre",{"className":158,"code":159,"language":160,"meta":48,"style":48},"language-typescript shiki shiki-themes material-theme-lighter github-light github-dark","const organization = \"qhseportal\";\nconst workflowName = \"UK Health & Safety Review\";\nconst numberOfWorkflows = 173; \u002F\u002F The magic number!\n\n\u002F\u002F Create workflows and extract links\nfor (let i = 0; i \u003C numberOfWorkflows; i++) {\n  console.log(`Creating workflow ${i + 1} of ${numberOfWorkflows}`);\n\n  const workflow = await startNewWorkflow(\n    tenant.id,\n    concerendWorkflow?.workflowCollectionId,\n    concerendWorkflow?.tenantTeamId,\n  );\n\n  const workflowProcess = await getWorkflowProcess({\n    workflowId: workflow.data.startWorkflowProcess.id,\n    tenantId: tenant.id,\n  });\n\n  \u002F\u002F Hunt for that precious anonymous link in the metadata\n  const anonymousLinkAvailable = workflowProcess.allTasks.find((task) =>\n    task.metadata.includes(\"anonymousLink\"),\n  );\n\n  if (anonymousLinkAvailable) {\n    const anonymousLinkMetadata = JSON.parse(anonymousLinkAvailable.metadata);\n    const anonymousLink = anonymousLinkMetadata.anonymousLink;\n\n    \u002F\u002F Save it for posterity (and the client)\n    const csvContent = `${i + 1},${anonymousLink}\\n`;\n    fs.appendFileSync(csvPath, csvContent);\n  }\n\n  \u002F\u002F Be nice to our API\n  await new Promise((resolve) => setTimeout(resolve, 1000));\n}\n","typescript",[162,163,164,196,214,235,241,247,293,346,351,371,385,399,411,419,424,443,470,487,497,502,508,543,572,579,584,599,630,648,653,659,696,720,726,731,737,779],"code",{"__ignoreMap":48},[165,166,169,173,177,181,185,189,192],"span",{"class":167,"line":168},"line",1,[165,170,172],{"class":171},"sbsja","const",[165,174,176],{"class":175},"s_hVV"," organization",[165,178,180],{"class":179},"smGrS"," =",[165,182,184],{"class":183},"sjJ54"," \"",[165,186,188],{"class":187},"s_sjI","qhseportal",[165,190,191],{"class":183},"\"",[165,193,195],{"class":194},"sP7_E",";\n",[165,197,198,200,203,205,207,210,212],{"class":167,"line":49},[165,199,172],{"class":171},[165,201,202],{"class":175}," workflowName",[165,204,180],{"class":179},[165,206,184],{"class":183},[165,208,209],{"class":187},"UK Health & Safety Review",[165,211,191],{"class":183},[165,213,195],{"class":194},[165,215,217,219,222,224,228,231],{"class":167,"line":216},3,[165,218,172],{"class":171},[165,220,221],{"class":175}," numberOfWorkflows",[165,223,180],{"class":179},[165,225,227],{"class":226},"srdBf"," 173",[165,229,230],{"class":194},";",[165,232,234],{"class":233},"sutJx"," \u002F\u002F The magic number!\n",[165,236,238],{"class":167,"line":237},4,[165,239,240],{"emptyLinePlaceholder":56},"\n",[165,242,244],{"class":167,"line":243},5,[165,245,246],{"class":233},"\u002F\u002F Create workflows and extract links\n",[165,248,250,254,258,261,264,267,270,272,274,277,279,281,284,287,290],{"class":167,"line":249},6,[165,251,253],{"class":252},"sVHd0","for",[165,255,257],{"class":256},"su5hD"," (",[165,259,260],{"class":171},"let",[165,262,263],{"class":256}," i ",[165,265,266],{"class":179},"=",[165,268,269],{"class":226}," 0",[165,271,230],{"class":194},[165,273,263],{"class":256},[165,275,276],{"class":179},"\u003C",[165,278,221],{"class":256},[165,280,230],{"class":194},[165,282,283],{"class":256}," i",[165,285,286],{"class":179},"++",[165,288,289],{"class":256},") ",[165,291,292],{"class":194},"{\n",[165,294,296,299,301,305,309,312,315,318,321,324,327,330,333,335,338,341,344],{"class":167,"line":295},7,[165,297,298],{"class":256},"  console",[165,300,46],{"class":194},[165,302,304],{"class":303},"sGLFI","log",[165,306,308],{"class":307},"skxfh","(",[165,310,311],{"class":183},"`",[165,313,314],{"class":187},"Creating workflow ",[165,316,317],{"class":183},"${",[165,319,320],{"class":256},"i",[165,322,323],{"class":179}," +",[165,325,326],{"class":226}," 1",[165,328,329],{"class":183},"}",[165,331,332],{"class":187}," of ",[165,334,317],{"class":183},[165,336,337],{"class":256},"numberOfWorkflows",[165,339,340],{"class":183},"}`",[165,342,343],{"class":307},")",[165,345,195],{"class":194},[165,347,349],{"class":167,"line":348},8,[165,350,240],{"emptyLinePlaceholder":56},[165,352,354,357,360,362,365,368],{"class":167,"line":353},9,[165,355,356],{"class":171},"  const",[165,358,359],{"class":175}," workflow",[165,361,180],{"class":179},[165,363,364],{"class":252}," await",[165,366,367],{"class":303}," startNewWorkflow",[165,369,370],{"class":307},"(\n",[165,372,374,377,379,382],{"class":167,"line":373},10,[165,375,376],{"class":256},"    tenant",[165,378,46],{"class":194},[165,380,381],{"class":256},"id",[165,383,384],{"class":194},",\n",[165,386,388,391,394,397],{"class":167,"line":387},11,[165,389,390],{"class":256},"    concerendWorkflow",[165,392,393],{"class":194},"?.",[165,395,396],{"class":256},"workflowCollectionId",[165,398,384],{"class":194},[165,400,402,404,406,409],{"class":167,"line":401},12,[165,403,390],{"class":256},[165,405,393],{"class":194},[165,407,408],{"class":256},"tenantTeamId",[165,410,384],{"class":194},[165,412,414,417],{"class":167,"line":413},13,[165,415,416],{"class":307},"  )",[165,418,195],{"class":194},[165,420,422],{"class":167,"line":421},14,[165,423,240],{"emptyLinePlaceholder":56},[165,425,427,429,432,434,436,439,441],{"class":167,"line":426},15,[165,428,356],{"class":171},[165,430,431],{"class":175}," workflowProcess",[165,433,180],{"class":179},[165,435,364],{"class":252},[165,437,438],{"class":303}," getWorkflowProcess",[165,440,308],{"class":307},[165,442,292],{"class":194},[165,444,446,449,452,454,456,459,461,464,466,468],{"class":167,"line":445},16,[165,447,448],{"class":307},"    workflowId",[165,450,451],{"class":194},":",[165,453,359],{"class":256},[165,455,46],{"class":194},[165,457,458],{"class":256},"data",[165,460,46],{"class":194},[165,462,463],{"class":256},"startWorkflowProcess",[165,465,46],{"class":194},[165,467,381],{"class":256},[165,469,384],{"class":194},[165,471,473,476,478,481,483,485],{"class":167,"line":472},17,[165,474,475],{"class":307},"    tenantId",[165,477,451],{"class":194},[165,479,480],{"class":256}," tenant",[165,482,46],{"class":194},[165,484,381],{"class":256},[165,486,384],{"class":194},[165,488,490,493,495],{"class":167,"line":489},18,[165,491,492],{"class":194},"  }",[165,494,343],{"class":307},[165,496,195],{"class":194},[165,498,500],{"class":167,"line":499},19,[165,501,240],{"emptyLinePlaceholder":56},[165,503,505],{"class":167,"line":504},20,[165,506,507],{"class":233},"  \u002F\u002F Hunt for that precious anonymous link in the metadata\n",[165,509,511,513,516,518,520,522,525,527,530,532,534,538,540],{"class":167,"line":510},21,[165,512,356],{"class":171},[165,514,515],{"class":175}," anonymousLinkAvailable",[165,517,180],{"class":179},[165,519,431],{"class":256},[165,521,46],{"class":194},[165,523,524],{"class":256},"allTasks",[165,526,46],{"class":194},[165,528,529],{"class":303},"find",[165,531,308],{"class":307},[165,533,308],{"class":194},[165,535,537],{"class":536},"s99_P","task",[165,539,343],{"class":194},[165,541,542],{"class":171}," =>\n",[165,544,546,549,551,554,556,559,561,563,566,568,570],{"class":167,"line":545},22,[165,547,548],{"class":256},"    task",[165,550,46],{"class":194},[165,552,553],{"class":256},"metadata",[165,555,46],{"class":194},[165,557,558],{"class":303},"includes",[165,560,308],{"class":307},[165,562,191],{"class":183},[165,564,565],{"class":187},"anonymousLink",[165,567,191],{"class":183},[165,569,343],{"class":307},[165,571,384],{"class":194},[165,573,575,577],{"class":167,"line":574},23,[165,576,416],{"class":307},[165,578,195],{"class":194},[165,580,582],{"class":167,"line":581},24,[165,583,240],{"emptyLinePlaceholder":56},[165,585,587,590,592,595,597],{"class":167,"line":586},25,[165,588,589],{"class":252},"  if",[165,591,257],{"class":307},[165,593,594],{"class":256},"anonymousLinkAvailable",[165,596,289],{"class":307},[165,598,292],{"class":194},[165,600,602,605,608,610,613,615,618,620,622,624,626,628],{"class":167,"line":601},26,[165,603,604],{"class":171},"    const",[165,606,607],{"class":175}," anonymousLinkMetadata",[165,609,180],{"class":179},[165,611,612],{"class":175}," JSON",[165,614,46],{"class":194},[165,616,617],{"class":303},"parse",[165,619,308],{"class":307},[165,621,594],{"class":256},[165,623,46],{"class":194},[165,625,553],{"class":256},[165,627,343],{"class":307},[165,629,195],{"class":194},[165,631,633,635,638,640,642,644,646],{"class":167,"line":632},27,[165,634,604],{"class":171},[165,636,637],{"class":175}," anonymousLink",[165,639,180],{"class":179},[165,641,607],{"class":256},[165,643,46],{"class":194},[165,645,565],{"class":256},[165,647,195],{"class":194},[165,649,651],{"class":167,"line":650},28,[165,652,240],{"emptyLinePlaceholder":56},[165,654,656],{"class":167,"line":655},29,[165,657,658],{"class":233},"    \u002F\u002F Save it for posterity (and the client)\n",[165,660,662,664,667,669,672,674,676,678,680,683,685,687,689,692,694],{"class":167,"line":661},30,[165,663,604],{"class":171},[165,665,666],{"class":175}," csvContent",[165,668,180],{"class":179},[165,670,671],{"class":183}," `${",[165,673,320],{"class":256},[165,675,323],{"class":179},[165,677,326],{"class":226},[165,679,329],{"class":183},[165,681,682],{"class":187},",",[165,684,317],{"class":183},[165,686,565],{"class":256},[165,688,329],{"class":183},[165,690,691],{"class":175},"\\n",[165,693,311],{"class":183},[165,695,195],{"class":194},[165,697,699,702,704,707,709,712,714,716,718],{"class":167,"line":698},31,[165,700,701],{"class":256},"    fs",[165,703,46],{"class":194},[165,705,706],{"class":303},"appendFileSync",[165,708,308],{"class":307},[165,710,711],{"class":256},"csvPath",[165,713,682],{"class":194},[165,715,666],{"class":256},[165,717,343],{"class":307},[165,719,195],{"class":194},[165,721,723],{"class":167,"line":722},32,[165,724,725],{"class":194},"  }\n",[165,727,729],{"class":167,"line":728},33,[165,730,240],{"emptyLinePlaceholder":56},[165,732,734],{"class":167,"line":733},34,[165,735,736],{"class":233},"  \u002F\u002F Be nice to our API\n",[165,738,740,743,746,750,752,754,757,759,762,765,767,769,771,774,777],{"class":167,"line":739},35,[165,741,742],{"class":252},"  await",[165,744,745],{"class":179}," new",[165,747,749],{"class":748},"sZMiF"," Promise",[165,751,308],{"class":307},[165,753,308],{"class":194},[165,755,756],{"class":536},"resolve",[165,758,343],{"class":194},[165,760,761],{"class":171}," =>",[165,763,764],{"class":303}," setTimeout",[165,766,308],{"class":307},[165,768,756],{"class":256},[165,770,682],{"class":194},[165,772,773],{"class":226}," 1000",[165,775,776],{"class":307},"))",[165,778,195],{"class":194},[165,780,782],{"class":167,"line":781},36,[165,783,784],{"class":194},"}\n",[148,786,788],{"id":787},"thoughtful-details","Thoughtful Details",[102,790,791],{},[105,792,793,797],{},[794,795,796],"strong",{},"Rate Limiting",": Added a 1-second delay between requests because being a good API citizen is important!",[156,799,801],{"className":158,"code":800,"language":160,"meta":48,"style":48},"await new Promise((resolve) => setTimeout(resolve, 1000));\n",[162,802,803],{"__ignoreMap":48},[165,804,805,808,810,812,814,816,818,820,822,824,827,829,831,833],{"class":167,"line":168},[165,806,807],{"class":252},"await",[165,809,745],{"class":179},[165,811,749],{"class":748},[165,813,308],{"class":256},[165,815,308],{"class":194},[165,817,756],{"class":536},[165,819,343],{"class":194},[165,821,761],{"class":171},[165,823,764],{"class":303},[165,825,826],{"class":256},"(resolve",[165,828,682],{"class":194},[165,830,773],{"class":226},[165,832,776],{"class":256},[165,834,195],{"class":194},[102,836,837],{"start":49},[105,838,839,842],{},[794,840,841],{},"Real-time Progress",": Because watching numbers go up is satisfying:",[156,844,846],{"className":158,"code":845,"language":160,"meta":48,"style":48},"console.log(`Creating workflow ${i + 1} of ${numberOfWorkflows}`);\n",[162,847,848],{"__ignoreMap":48},[165,849,850,853,855,857,859,861,863,865,867,869,871,873,875,877,879,881,883],{"class":167,"line":168},[165,851,852],{"class":256},"console",[165,854,46],{"class":194},[165,856,304],{"class":303},[165,858,308],{"class":256},[165,860,311],{"class":183},[165,862,314],{"class":187},[165,864,317],{"class":183},[165,866,320],{"class":256},[165,868,323],{"class":179},[165,870,326],{"class":226},[165,872,329],{"class":183},[165,874,332],{"class":187},[165,876,317],{"class":183},[165,878,337],{"class":256},[165,880,340],{"class":183},[165,882,343],{"class":256},[165,884,195],{"class":194},[102,886,887],{"start":216},[105,888,889,892],{},[794,890,891],{},"Timestamp-based Files",": Each run creates a unique CSV file:",[156,894,896],{"className":158,"code":895,"language":160,"meta":48,"style":48},"const timestamp = new Date().toISOString().replace(\u002F[:.]\u002Fg, \"-\");\nconst csvPath = path.join(__dirname, `anonymousLinks-${timestamp}.csv`);\n",[162,897,898,962],{"__ignoreMap":48},[165,899,900,902,905,907,909,912,915,917,920,922,924,927,929,932,936,940,943,945,949,951,953,956,958,960],{"class":167,"line":168},[165,901,172],{"class":171},[165,903,904],{"class":175}," timestamp",[165,906,180],{"class":179},[165,908,745],{"class":179},[165,910,911],{"class":303}," Date",[165,913,914],{"class":256},"()",[165,916,46],{"class":194},[165,918,919],{"class":303},"toISOString",[165,921,914],{"class":256},[165,923,46],{"class":194},[165,925,926],{"class":303},"replace",[165,928,308],{"class":256},[165,930,931],{"class":183},"\u002F",[165,933,935],{"class":934},"s39Yj","[",[165,937,939],{"class":938},"stzsN",":.",[165,941,942],{"class":934},"]",[165,944,931],{"class":183},[165,946,948],{"class":947},"sw1J6","g",[165,950,682],{"class":194},[165,952,184],{"class":183},[165,954,955],{"class":187},"-",[165,957,191],{"class":183},[165,959,343],{"class":256},[165,961,195],{"class":194},[165,963,964,966,969,971,974,976,979,982,984,987,990,992,995,997,1000,1002,1004],{"class":167,"line":49},[165,965,172],{"class":171},[165,967,968],{"class":175}," csvPath",[165,970,180],{"class":179},[165,972,973],{"class":256}," path",[165,975,46],{"class":194},[165,977,978],{"class":303},"join",[165,980,981],{"class":256},"(__dirname",[165,983,682],{"class":194},[165,985,986],{"class":183}," `",[165,988,989],{"class":187},"anonymousLinks-",[165,991,317],{"class":183},[165,993,994],{"class":256},"timestamp",[165,996,329],{"class":183},[165,998,999],{"class":187},".csv",[165,1001,311],{"class":183},[165,1003,343],{"class":256},[165,1005,195],{"class":194},[19,1007,1009],{"id":1008},"the-results","The Results",[15,1011,1012],{},"The script turned what would have been hours of mind-numbing clicking into a 5-minute automated process. The output? A beautiful CSV file with rows of anonymous links, ready to be handed to the client:",[156,1014,1018],{"className":1015,"code":1016,"language":1017,"meta":48,"style":48},"language-csv shiki shiki-themes material-theme-lighter github-light github-dark","Row,AnonymousLink\n1,https:\u002F\u002Fcapptions.direct\u002F...\u002Fanonymous-task\u002Fc2d7556c-...\n2,https:\u002F\u002Fcapptions.direct\u002F...\u002Fanonymous-task\u002F1103443e-...\n...\n173,https:\u002F\u002Fcapptions.direct\u002F...\u002Fanonymous-task\u002Fa5dc22ca-...\n","csv",[162,1019,1020,1025,1030,1035,1040],{"__ignoreMap":48},[165,1021,1022],{"class":167,"line":168},[165,1023,1024],{},"Row,AnonymousLink\n",[165,1026,1027],{"class":167,"line":49},[165,1028,1029],{},"1,https:\u002F\u002Fcapptions.direct\u002F...\u002Fanonymous-task\u002Fc2d7556c-...\n",[165,1031,1032],{"class":167,"line":216},[165,1033,1034],{},"2,https:\u002F\u002Fcapptions.direct\u002F...\u002Fanonymous-task\u002F1103443e-...\n",[165,1036,1037],{"class":167,"line":237},[165,1038,1039],{},"...\n",[165,1041,1042],{"class":167,"line":243},[165,1043,1044],{},"173,https:\u002F\u002Fcapptions.direct\u002F...\u002Fanonymous-task\u002Fa5dc22ca-...\n",[19,1046,1048],{"id":1047},"what-i-learned","What I Learned",[102,1050,1051,1057,1063,1081],{},[105,1052,1053,1056],{},[794,1054,1055],{},"The Power of Workarounds",": Sometimes the best solution isn't waiting for the \"proper\" feature but finding creative ways to use what you have.",[105,1058,1059,1062],{},[794,1060,1061],{},"API Understanding is Crucial",": By understanding our platform's GraphQL API flow, I could automate what was designed to be a manual process.",[105,1064,1065,1068,1069],{},[794,1066,1067],{},"Customer Success Partnership",": Working closely with CSM team led to a solution that:",[1070,1071,1072,1075,1078],"ul",{},[105,1073,1074],{},"Saved them time",[105,1076,1077],{},"Made customers happy",[105,1079,1080],{},"Bought development time for the proper feature",[105,1082,1083,1086,1087,1090,1091,1094],{},[794,1084,1085],{},"Rate Limiting is Your Friend",": Just because you ",[83,1088,1089],{},"can"," hammer an API doesn't mean you ",[83,1092,1093],{},"should",". A small delay between requests makes everyone happier... and avoid network related issues (ew!).",[19,1096,1098],{"id":1097},"impact-reusability","Impact & Reusability",[15,1100,1101],{},"The script became something of a hit with our CSM team. They came back multiple times for different clients needing bulk anonymous links. Each time, it was just a matter of:",[102,1103,1104,1107,1110],{},[105,1105,1106],{},"Update the organization and workflow name",[105,1108,1109],{},"Set the number of links needed",[105,1111,1112],{},"Run and wait for the CSV",[15,1114,1115],{},"It was a perfect example of how a small automation script can have an outsized impact on customer satisfaction and team efficiency.",[19,1117,1119],{"id":1118},"whats-next","What's Next?",[15,1121,1122],{},"While this script served its purpose beautifully (and got multiple encores!), it was always meant to be a temporary solution. The proper feature has since been built into the platform, making this script a fond memory of creative problem-solving.",[15,1124,1125],{},"But the lesson lives on: Sometimes the best solutions come not from building the perfect feature, but from creatively using what you have to solve immediate problems.",[19,1127,1129],{"id":1128},"key-takeaways","Key Takeaways",[15,1131,1132],{},"Well, a little realisation was that being a good developer isn't just about writing the best or most efficient code - it's about solving problems. Sometimes that means building new features, and sometimes it means finding clever ways to make existing features do new tricks. ...or just automating the heck out of your platform with its current GQL APIs. 😉",[15,1134,1135],{},"The script might have been temporary, but it:",[1070,1137,1138,1141,1144,1147],{},[105,1139,1140],{},"Solved an immediate customer need",[105,1142,1143],{},"Gave the development team breathing room",[105,1145,1146],{},"Provided valuable insights for the eventual feature",[105,1148,1149],{},"Made our CSM team's life easier",[15,1151,1152],{},"And really, isn't that what good automation is all about?",[1154,1155],"hr",{},[15,1157,1158],{},[83,1159,1160],{},"Note: This case study focuses on the technical implementation while respecting confidentiality around specific client details. The script has since been retired in favor of proper platform features, but its legacy lives on in happy customers and satisfied CSMs.",[1162,1163,1164],"style",{},"html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .s39Yj, html code.shiki .s39Yj{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .stzsN, html code.shiki .stzsN{--shiki-light:#91B859;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sw1J6, html code.shiki .sw1J6{--shiki-light:#F76D47;--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":48,"searchDepth":49,"depth":49,"links":1166},[1167,1168,1169,1173,1174,1175,1176,1177],{"id":77,"depth":49,"text":78},{"id":96,"depth":49,"text":97},{"id":125,"depth":49,"text":126,"children":1170},[1171,1172],{"id":150,"depth":216,"text":151},{"id":787,"depth":216,"text":788},{"id":1008,"depth":49,"text":1009},{"id":1047,"depth":49,"text":1048},{"id":1097,"depth":49,"text":1098},{"id":1118,"depth":49,"text":1119},{"id":1128,"depth":49,"text":1129},"2025-06-18","Built a creative automation solution to generate hundreds of anonymous form links, bridging the gap between customer needs and platform capabilities while the proper feature was in development.",null,false,"case-study",{"slug":1184},"anonymous-link-generator","\u002Fwork\u002Fanonymous-link-generator",{"title":69,"description":1179},"work\u002Fanonymous-link-generator",[64,1189,1190,1191,1192],"Automation","GraphQL","CSV","Scripting","VAcaPGkEAHeciskAjWdaAcABzXjzQnntd7-h9GDhe_Y",{"id":1195,"title":1196,"body":1197,"date":2442,"description":2443,"extension":55,"externalUrl":1180,"featured":1181,"kind":1182,"meta":2444,"navigation":56,"path":2446,"seo":2447,"stem":2448,"tags":2449,"__hash__":2451},"work\u002Fwork\u002Fconditional-form-builder-automation.md","LLM-powered form builder for defense suppliers",{"type":8,"value":1198,"toc":2431},[1199,1203,1205,1208,1215,1217,1220,1234,1237,1241,1245,1270,1274,1281,1516,1519,1530,1537,2020,2023,2037,2044,2255,2258,2272,2274,2281,2284,2298,2305,2308,2322,2329,2332,2346,2348,2407,2409,2415,2418,2421,2423,2428],[11,1200,1202],{"id":1201},"building-complex-conditional-forms-using-llms-for-type-analysis","Building complex conditional forms + using LLM's for type analysis",[19,1204,78],{"id":77},[15,1206,1207],{},"Our platform needed to publish a Defense Supplier Checklist as a marketplace offering. The challenge? Building a complex form with 26 interdependent questions, each requiring specific conditional logic based on previous answers. The kicker: it needed to be done under significant time pressure.",[87,1209,1210],{},[15,1211,1212],{},[83,1213,1214],{},"Note: While specific implementation details have been anonymized, this case study focuses on the technical approach to solving a common challenge in form builders: managing complex conditional logic while maintaining type safety and user experience.",[19,1216,126],{"id":125},[15,1218,1219],{},"I developed an automated form generation system that could:",[102,1221,1222,1225,1228,1231],{},[105,1223,1224],{},"Create hierarchical form structures with complex conditional logic",[105,1226,1227],{},"Handle recursive dependencies between form fields",[105,1229,1230],{},"Maintain type safety across the entire form structure",[105,1232,1233],{},"Generate proper GraphQL mutations for form creation",[15,1235,1236],{},"The end result was a fully functional, conditional form workflow that guides users through a complex compliance checklist.",[19,1238,1240],{"id":1239},"technical-breakdown","Technical Breakdown",[148,1242,1244],{"id":1243},"stack-tools","Stack & Tools",[1070,1246,1247,1253,1258,1264],{},[105,1248,1249,1252],{},[794,1250,1251],{},"TypeScript\u002FNode.js",": Core implementation",[105,1254,1255,1257],{},[794,1256,1190],{},": Platform API integration",[105,1259,1260,1263],{},[794,1261,1262],{},"LLM",": Type analysis and condition generation",[105,1265,1266,1269],{},[794,1267,1268],{},"Form Builder API",": Custom platform form creation",[148,1271,1273],{"id":1272},"key-architecture-decisions","Key Architecture Decisions",[102,1275,1276],{},[105,1277,1278],{},[794,1279,1280],{},"Field ID Management System",[156,1282,1284],{"className":158,"code":1283,"language":160,"meta":48,"style":48},"interface FieldIds {\n  [key: string]: {\n    id: string;\n    slug: string;\n  };\n}\n\n\u002F\u002F Store and track field IDs for conditions\nconst fieldIds: FieldIds = {};\n\n\u002F\u002F Create fields and store references\nconst choiceField = await createSingleChoiceField(\n  tenantId,\n  section.id,\n  `q${questionNumber}`,\n  label,\n  conditions,\n);\n\nfieldIds[`q${questionNumber}`] = {\n  id: choiceField.id,\n  slug: choiceField.slug,\n};\n",[162,1285,1286,1298,1317,1329,1340,1345,1349,1353,1358,1374,1378,1383,1399,1406,1417,1434,1441,1448,1454,1458,1480,1495,1511],{"__ignoreMap":48},[165,1287,1288,1291,1295],{"class":167,"line":168},[165,1289,1290],{"class":171},"interface",[165,1292,1294],{"class":1293},"sbgvK"," FieldIds",[165,1296,1297],{"class":194}," {\n",[165,1299,1300,1303,1306,1308,1311,1313,1315],{"class":167,"line":49},[165,1301,1302],{"class":256},"  [",[165,1304,1305],{"class":536},"key",[165,1307,451],{"class":179},[165,1309,1310],{"class":748}," string",[165,1312,942],{"class":256},[165,1314,451],{"class":179},[165,1316,1297],{"class":194},[165,1318,1319,1323,1325,1327],{"class":167,"line":216},[165,1320,1322],{"class":1321},"sucvu","    id",[165,1324,451],{"class":179},[165,1326,1310],{"class":748},[165,1328,195],{"class":194},[165,1330,1331,1334,1336,1338],{"class":167,"line":237},[165,1332,1333],{"class":1321},"    slug",[165,1335,451],{"class":179},[165,1337,1310],{"class":748},[165,1339,195],{"class":194},[165,1341,1342],{"class":167,"line":243},[165,1343,1344],{"class":194},"  };\n",[165,1346,1347],{"class":167,"line":249},[165,1348,784],{"class":194},[165,1350,1351],{"class":167,"line":295},[165,1352,240],{"emptyLinePlaceholder":56},[165,1354,1355],{"class":167,"line":348},[165,1356,1357],{"class":233},"\u002F\u002F Store and track field IDs for conditions\n",[165,1359,1360,1362,1365,1367,1369,1371],{"class":167,"line":353},[165,1361,172],{"class":171},[165,1363,1364],{"class":175}," fieldIds",[165,1366,451],{"class":179},[165,1368,1294],{"class":1293},[165,1370,180],{"class":179},[165,1372,1373],{"class":194}," {};\n",[165,1375,1376],{"class":167,"line":373},[165,1377,240],{"emptyLinePlaceholder":56},[165,1379,1380],{"class":167,"line":387},[165,1381,1382],{"class":233},"\u002F\u002F Create fields and store references\n",[165,1384,1385,1387,1390,1392,1394,1397],{"class":167,"line":401},[165,1386,172],{"class":171},[165,1388,1389],{"class":175}," choiceField",[165,1391,180],{"class":179},[165,1393,364],{"class":252},[165,1395,1396],{"class":303}," createSingleChoiceField",[165,1398,370],{"class":256},[165,1400,1401,1404],{"class":167,"line":413},[165,1402,1403],{"class":256},"  tenantId",[165,1405,384],{"class":194},[165,1407,1408,1411,1413,1415],{"class":167,"line":421},[165,1409,1410],{"class":256},"  section",[165,1412,46],{"class":194},[165,1414,381],{"class":256},[165,1416,384],{"class":194},[165,1418,1419,1422,1425,1427,1430,1432],{"class":167,"line":426},[165,1420,1421],{"class":183},"  `",[165,1423,1424],{"class":187},"q",[165,1426,317],{"class":183},[165,1428,1429],{"class":256},"questionNumber",[165,1431,340],{"class":183},[165,1433,384],{"class":194},[165,1435,1436,1439],{"class":167,"line":445},[165,1437,1438],{"class":256},"  label",[165,1440,384],{"class":194},[165,1442,1443,1446],{"class":167,"line":472},[165,1444,1445],{"class":256},"  conditions",[165,1447,384],{"class":194},[165,1449,1450,1452],{"class":167,"line":489},[165,1451,343],{"class":256},[165,1453,195],{"class":194},[165,1455,1456],{"class":167,"line":499},[165,1457,240],{"emptyLinePlaceholder":56},[165,1459,1460,1463,1465,1467,1469,1471,1473,1476,1478],{"class":167,"line":504},[165,1461,1462],{"class":256},"fieldIds[",[165,1464,311],{"class":183},[165,1466,1424],{"class":187},[165,1468,317],{"class":183},[165,1470,1429],{"class":256},[165,1472,340],{"class":183},[165,1474,1475],{"class":256},"] ",[165,1477,266],{"class":179},[165,1479,1297],{"class":194},[165,1481,1482,1485,1487,1489,1491,1493],{"class":167,"line":510},[165,1483,1484],{"class":307},"  id",[165,1486,451],{"class":194},[165,1488,1389],{"class":256},[165,1490,46],{"class":194},[165,1492,381],{"class":256},[165,1494,384],{"class":194},[165,1496,1497,1500,1502,1504,1506,1509],{"class":167,"line":545},[165,1498,1499],{"class":307},"  slug",[165,1501,451],{"class":194},[165,1503,1389],{"class":256},[165,1505,46],{"class":194},[165,1507,1508],{"class":256},"slug",[165,1510,384],{"class":194},[165,1512,1513],{"class":167,"line":574},[165,1514,1515],{"class":194},"};\n",[15,1517,1518],{},"This system:",[1070,1520,1521,1524,1527],{},[105,1522,1523],{},"Tracks all created field IDs and slugs",[105,1525,1526],{},"Enables reference to previous fields in conditions",[105,1528,1529],{},"Maintains type safety for field references",[102,1531,1532],{"start":49},[105,1533,1534],{},[794,1535,1536],{},"Complex Conditional Logic",[156,1538,1540],{"className":158,"code":1539,"language":160,"meta":48,"style":48},"\u002F\u002F Example of nested conditional logic\nconst conditions = {\n  query: {\n    expression: {\n      condition: \"AND\",\n      rules: Array.from({ length: 25 }, (_, idx) => ({\n        id: fieldIds[`q${(idx + 1).toString().padStart(2, \"0\")}`].id,\n        operator: \"EQUALS_TO_OPTION\",\n        value: \"1\",\n        slug: fieldIds[`q${(idx + 1).toString().padStart(2, \"0\")}`].slug,\n      })),\n    },\n  },\n};\n\n\u002F\u002F Field creation with conditions\nawait updateFormField(tenantId, field.id, {\n  richTitle: label,\n  metadata: {\n    type: \"SELECT\",\n    options: [\n      { label: \"Ja\u002FYes\", value: \"1\" },\n      { label: \"Onderhanden\u002FWIP\", value: \"2\" },\n      { label: \"Nee\u002FNo\", value: \"0\" },\n    ],\n    visible: conditions,\n  },\n});\n",[162,1541,1542,1547,1558,1567,1576,1592,1641,1707,1723,1739,1798,1807,1812,1817,1821,1825,1830,1853,1865,1874,1890,1900,1932,1961,1990,1997,2008,2012],{"__ignoreMap":48},[165,1543,1544],{"class":167,"line":168},[165,1545,1546],{"class":233},"\u002F\u002F Example of nested conditional logic\n",[165,1548,1549,1551,1554,1556],{"class":167,"line":49},[165,1550,172],{"class":171},[165,1552,1553],{"class":175}," conditions",[165,1555,180],{"class":179},[165,1557,1297],{"class":194},[165,1559,1560,1563,1565],{"class":167,"line":216},[165,1561,1562],{"class":307},"  query",[165,1564,451],{"class":194},[165,1566,1297],{"class":194},[165,1568,1569,1572,1574],{"class":167,"line":237},[165,1570,1571],{"class":307},"    expression",[165,1573,451],{"class":194},[165,1575,1297],{"class":194},[165,1577,1578,1581,1583,1585,1588,1590],{"class":167,"line":243},[165,1579,1580],{"class":307},"      condition",[165,1582,451],{"class":194},[165,1584,184],{"class":183},[165,1586,1587],{"class":187},"AND",[165,1589,191],{"class":183},[165,1591,384],{"class":194},[165,1593,1594,1597,1599,1602,1604,1607,1609,1612,1615,1617,1620,1623,1625,1628,1630,1633,1635,1637,1639],{"class":167,"line":249},[165,1595,1596],{"class":307},"      rules",[165,1598,451],{"class":194},[165,1600,1601],{"class":256}," Array",[165,1603,46],{"class":194},[165,1605,1606],{"class":303},"from",[165,1608,308],{"class":256},[165,1610,1611],{"class":194},"{",[165,1613,1614],{"class":307}," length",[165,1616,451],{"class":194},[165,1618,1619],{"class":226}," 25",[165,1621,1622],{"class":194}," },",[165,1624,257],{"class":194},[165,1626,1627],{"class":536},"_",[165,1629,682],{"class":194},[165,1631,1632],{"class":536}," idx",[165,1634,343],{"class":194},[165,1636,761],{"class":171},[165,1638,257],{"class":256},[165,1640,292],{"class":194},[165,1642,1643,1646,1648,1651,1653,1655,1657,1660,1663,1665,1667,1669,1671,1674,1676,1678,1681,1683,1686,1688,1690,1693,1695,1697,1699,1701,1703,1705],{"class":167,"line":295},[165,1644,1645],{"class":307},"        id",[165,1647,451],{"class":194},[165,1649,1650],{"class":256}," fieldIds[",[165,1652,311],{"class":183},[165,1654,1424],{"class":187},[165,1656,317],{"class":183},[165,1658,308],{"class":1659},"sfo-9",[165,1661,1662],{"class":256},"idx",[165,1664,323],{"class":179},[165,1666,326],{"class":226},[165,1668,343],{"class":1659},[165,1670,46],{"class":183},[165,1672,1673],{"class":303},"toString",[165,1675,914],{"class":1659},[165,1677,46],{"class":183},[165,1679,1680],{"class":303},"padStart",[165,1682,308],{"class":1659},[165,1684,1685],{"class":226},"2",[165,1687,682],{"class":183},[165,1689,184],{"class":183},[165,1691,1692],{"class":187},"0",[165,1694,191],{"class":183},[165,1696,343],{"class":1659},[165,1698,340],{"class":183},[165,1700,942],{"class":256},[165,1702,46],{"class":194},[165,1704,381],{"class":256},[165,1706,384],{"class":194},[165,1708,1709,1712,1714,1716,1719,1721],{"class":167,"line":348},[165,1710,1711],{"class":307},"        operator",[165,1713,451],{"class":194},[165,1715,184],{"class":183},[165,1717,1718],{"class":187},"EQUALS_TO_OPTION",[165,1720,191],{"class":183},[165,1722,384],{"class":194},[165,1724,1725,1728,1730,1732,1735,1737],{"class":167,"line":353},[165,1726,1727],{"class":307},"        value",[165,1729,451],{"class":194},[165,1731,184],{"class":183},[165,1733,1734],{"class":187},"1",[165,1736,191],{"class":183},[165,1738,384],{"class":194},[165,1740,1741,1744,1746,1748,1750,1752,1754,1756,1758,1760,1762,1764,1766,1768,1770,1772,1774,1776,1778,1780,1782,1784,1786,1788,1790,1792,1794,1796],{"class":167,"line":373},[165,1742,1743],{"class":307},"        slug",[165,1745,451],{"class":194},[165,1747,1650],{"class":256},[165,1749,311],{"class":183},[165,1751,1424],{"class":187},[165,1753,317],{"class":183},[165,1755,308],{"class":1659},[165,1757,1662],{"class":256},[165,1759,323],{"class":179},[165,1761,326],{"class":226},[165,1763,343],{"class":1659},[165,1765,46],{"class":183},[165,1767,1673],{"class":303},[165,1769,914],{"class":1659},[165,1771,46],{"class":183},[165,1773,1680],{"class":303},[165,1775,308],{"class":1659},[165,1777,1685],{"class":226},[165,1779,682],{"class":183},[165,1781,184],{"class":183},[165,1783,1692],{"class":187},[165,1785,191],{"class":183},[165,1787,343],{"class":1659},[165,1789,340],{"class":183},[165,1791,942],{"class":256},[165,1793,46],{"class":194},[165,1795,1508],{"class":256},[165,1797,384],{"class":194},[165,1799,1800,1803,1805],{"class":167,"line":387},[165,1801,1802],{"class":194},"      }",[165,1804,776],{"class":256},[165,1806,384],{"class":194},[165,1808,1809],{"class":167,"line":401},[165,1810,1811],{"class":194},"    },\n",[165,1813,1814],{"class":167,"line":413},[165,1815,1816],{"class":194},"  },\n",[165,1818,1819],{"class":167,"line":421},[165,1820,1515],{"class":194},[165,1822,1823],{"class":167,"line":426},[165,1824,240],{"emptyLinePlaceholder":56},[165,1826,1827],{"class":167,"line":445},[165,1828,1829],{"class":233},"\u002F\u002F Field creation with conditions\n",[165,1831,1832,1834,1837,1840,1842,1845,1847,1849,1851],{"class":167,"line":472},[165,1833,807],{"class":252},[165,1835,1836],{"class":303}," updateFormField",[165,1838,1839],{"class":256},"(tenantId",[165,1841,682],{"class":194},[165,1843,1844],{"class":256}," field",[165,1846,46],{"class":194},[165,1848,381],{"class":256},[165,1850,682],{"class":194},[165,1852,1297],{"class":194},[165,1854,1855,1858,1860,1863],{"class":167,"line":489},[165,1856,1857],{"class":307},"  richTitle",[165,1859,451],{"class":194},[165,1861,1862],{"class":256}," label",[165,1864,384],{"class":194},[165,1866,1867,1870,1872],{"class":167,"line":499},[165,1868,1869],{"class":307},"  metadata",[165,1871,451],{"class":194},[165,1873,1297],{"class":194},[165,1875,1876,1879,1881,1883,1886,1888],{"class":167,"line":504},[165,1877,1878],{"class":307},"    type",[165,1880,451],{"class":194},[165,1882,184],{"class":183},[165,1884,1885],{"class":187},"SELECT",[165,1887,191],{"class":183},[165,1889,384],{"class":194},[165,1891,1892,1895,1897],{"class":167,"line":510},[165,1893,1894],{"class":307},"    options",[165,1896,451],{"class":194},[165,1898,1899],{"class":256}," [\n",[165,1901,1902,1905,1907,1909,1911,1914,1916,1918,1921,1923,1925,1927,1929],{"class":167,"line":545},[165,1903,1904],{"class":194},"      {",[165,1906,1862],{"class":307},[165,1908,451],{"class":194},[165,1910,184],{"class":183},[165,1912,1913],{"class":187},"Ja\u002FYes",[165,1915,191],{"class":183},[165,1917,682],{"class":194},[165,1919,1920],{"class":307}," value",[165,1922,451],{"class":194},[165,1924,184],{"class":183},[165,1926,1734],{"class":187},[165,1928,191],{"class":183},[165,1930,1931],{"class":194}," },\n",[165,1933,1934,1936,1938,1940,1942,1945,1947,1949,1951,1953,1955,1957,1959],{"class":167,"line":574},[165,1935,1904],{"class":194},[165,1937,1862],{"class":307},[165,1939,451],{"class":194},[165,1941,184],{"class":183},[165,1943,1944],{"class":187},"Onderhanden\u002FWIP",[165,1946,191],{"class":183},[165,1948,682],{"class":194},[165,1950,1920],{"class":307},[165,1952,451],{"class":194},[165,1954,184],{"class":183},[165,1956,1685],{"class":187},[165,1958,191],{"class":183},[165,1960,1931],{"class":194},[165,1962,1963,1965,1967,1969,1971,1974,1976,1978,1980,1982,1984,1986,1988],{"class":167,"line":581},[165,1964,1904],{"class":194},[165,1966,1862],{"class":307},[165,1968,451],{"class":194},[165,1970,184],{"class":183},[165,1972,1973],{"class":187},"Nee\u002FNo",[165,1975,191],{"class":183},[165,1977,682],{"class":194},[165,1979,1920],{"class":307},[165,1981,451],{"class":194},[165,1983,184],{"class":183},[165,1985,1692],{"class":187},[165,1987,191],{"class":183},[165,1989,1931],{"class":194},[165,1991,1992,1995],{"class":167,"line":586},[165,1993,1994],{"class":256},"    ]",[165,1996,384],{"class":194},[165,1998,1999,2002,2004,2006],{"class":167,"line":601},[165,2000,2001],{"class":307},"    visible",[165,2003,451],{"class":194},[165,2005,1553],{"class":256},[165,2007,384],{"class":194},[165,2009,2010],{"class":167,"line":632},[165,2011,1816],{"class":194},[165,2013,2014,2016,2018],{"class":167,"line":650},[165,2015,329],{"class":194},[165,2017,343],{"class":256},[165,2019,195],{"class":194},[15,2021,2022],{},"The conditional system handles:",[1070,2024,2025,2028,2031,2034],{},[105,2026,2027],{},"Multi-level dependencies",[105,2029,2030],{},"Complex AND\u002FOR logic",[105,2032,2033],{},"Dynamic visibility rules",[105,2035,2036],{},"Type-safe condition generation",[102,2038,2039],{"start":216},[105,2040,2041],{},[794,2042,2043],{},"LLM-Assisted Type Analysis",[156,2045,2047],{"className":158,"code":2046,"language":160,"meta":48,"style":48},"\u002F\u002F Example of type structure fed to LLM\ninterface FormFieldMetadata {\n  type: \"SELECT\" | \"TEXTAREA\" | \"FILE_INPUT\";\n  visible?: {\n    query: {\n      expression: {\n        condition: \"AND\" | \"OR\" | \"NOT\";\n        rules: Array\u003C{\n          fieldId: string;\n          operator: \"EQUALS_TO_OPTION\" | \"NOT_EQUALS_TO_OPTION\";\n          value: string;\n          slug: string;\n        }>;\n      };\n    };\n  };\n  \u002F\u002F ... other type definitions\n}\n",[162,2048,2049,2054,2063,2097,2107,2116,2125,2158,2170,2181,2205,2216,2227,2232,2237,2242,2246,2251],{"__ignoreMap":48},[165,2050,2051],{"class":167,"line":168},[165,2052,2053],{"class":233},"\u002F\u002F Example of type structure fed to LLM\n",[165,2055,2056,2058,2061],{"class":167,"line":49},[165,2057,1290],{"class":171},[165,2059,2060],{"class":1293}," FormFieldMetadata",[165,2062,1297],{"class":194},[165,2064,2065,2068,2070,2072,2074,2076,2079,2081,2084,2086,2088,2090,2093,2095],{"class":167,"line":216},[165,2066,2067],{"class":1321},"  type",[165,2069,451],{"class":179},[165,2071,184],{"class":183},[165,2073,1885],{"class":187},[165,2075,191],{"class":183},[165,2077,2078],{"class":179}," |",[165,2080,184],{"class":183},[165,2082,2083],{"class":187},"TEXTAREA",[165,2085,191],{"class":183},[165,2087,2078],{"class":179},[165,2089,184],{"class":183},[165,2091,2092],{"class":187},"FILE_INPUT",[165,2094,191],{"class":183},[165,2096,195],{"class":194},[165,2098,2099,2102,2105],{"class":167,"line":237},[165,2100,2101],{"class":1321},"  visible",[165,2103,2104],{"class":179},"?:",[165,2106,1297],{"class":194},[165,2108,2109,2112,2114],{"class":167,"line":243},[165,2110,2111],{"class":1321},"    query",[165,2113,451],{"class":179},[165,2115,1297],{"class":194},[165,2117,2118,2121,2123],{"class":167,"line":249},[165,2119,2120],{"class":1321},"      expression",[165,2122,451],{"class":179},[165,2124,1297],{"class":194},[165,2126,2127,2130,2132,2134,2136,2138,2140,2142,2145,2147,2149,2151,2154,2156],{"class":167,"line":295},[165,2128,2129],{"class":1321},"        condition",[165,2131,451],{"class":179},[165,2133,184],{"class":183},[165,2135,1587],{"class":187},[165,2137,191],{"class":183},[165,2139,2078],{"class":179},[165,2141,184],{"class":183},[165,2143,2144],{"class":187},"OR",[165,2146,191],{"class":183},[165,2148,2078],{"class":179},[165,2150,184],{"class":183},[165,2152,2153],{"class":187},"NOT",[165,2155,191],{"class":183},[165,2157,195],{"class":194},[165,2159,2160,2163,2165,2167],{"class":167,"line":348},[165,2161,2162],{"class":1321},"        rules",[165,2164,451],{"class":179},[165,2166,1601],{"class":1293},[165,2168,2169],{"class":194},"\u003C{\n",[165,2171,2172,2175,2177,2179],{"class":167,"line":353},[165,2173,2174],{"class":1321},"          fieldId",[165,2176,451],{"class":179},[165,2178,1310],{"class":748},[165,2180,195],{"class":194},[165,2182,2183,2186,2188,2190,2192,2194,2196,2198,2201,2203],{"class":167,"line":373},[165,2184,2185],{"class":1321},"          operator",[165,2187,451],{"class":179},[165,2189,184],{"class":183},[165,2191,1718],{"class":187},[165,2193,191],{"class":183},[165,2195,2078],{"class":179},[165,2197,184],{"class":183},[165,2199,2200],{"class":187},"NOT_EQUALS_TO_OPTION",[165,2202,191],{"class":183},[165,2204,195],{"class":194},[165,2206,2207,2210,2212,2214],{"class":167,"line":387},[165,2208,2209],{"class":1321},"          value",[165,2211,451],{"class":179},[165,2213,1310],{"class":748},[165,2215,195],{"class":194},[165,2217,2218,2221,2223,2225],{"class":167,"line":401},[165,2219,2220],{"class":1321},"          slug",[165,2222,451],{"class":179},[165,2224,1310],{"class":748},[165,2226,195],{"class":194},[165,2228,2229],{"class":167,"line":413},[165,2230,2231],{"class":194},"        }>;\n",[165,2233,2234],{"class":167,"line":421},[165,2235,2236],{"class":194},"      };\n",[165,2238,2239],{"class":167,"line":426},[165,2240,2241],{"class":194},"    };\n",[165,2243,2244],{"class":167,"line":445},[165,2245,1344],{"class":194},[165,2247,2248],{"class":167,"line":472},[165,2249,2250],{"class":233},"  \u002F\u002F ... other type definitions\n",[165,2252,2253],{"class":167,"line":489},[165,2254,784],{"class":194},[15,2256,2257],{},"Used LLM to:",[1070,2259,2260,2263,2266,2269],{},[105,2261,2262],{},"Analyze complex type definitions",[105,2264,2265],{},"Generate correct condition structures",[105,2267,2268],{},"Validate conditional logic",[105,2270,2271],{},"Suggest proper field configurations",[19,2273,1048],{"id":1047},[102,2275,2276],{},[105,2277,2278],{},[794,2279,2280],{},"Time-Constrained Development",[15,2282,2283],{},"When faced with tight deadlines, I learned to:",[1070,2285,2286,2289,2292,2295],{},[105,2287,2288],{},"Prioritize working solutions over perfect implementations",[105,2290,2291],{},"Leverage AI tools for rapid development",[105,2293,2294],{},"Focus on core functionality first",[105,2296,2297],{},"Use existing patterns where possible",[102,2299,2300],{"start":49},[105,2301,2302],{},[794,2303,2304],{},"Type System Analysis",[15,2306,2307],{},"The approach of feeding type definitions to LLMs proved valuable:",[1070,2309,2310,2313,2316,2319],{},[105,2311,2312],{},"Quick understanding of complex type systems",[105,2314,2315],{},"Accurate condition generation",[105,2317,2318],{},"Reduced trial and error",[105,2320,2321],{},"Faster development cycle",[102,2323,2324],{"start":216},[105,2325,2326],{},[794,2327,2328],{},"Conditional Form Architecture",[15,2330,2331],{},"Key insights about building conditional forms:",[1070,2333,2334,2337,2340,2343],{},[105,2335,2336],{},"Store field references immediately after creation",[105,2338,2339],{},"Build conditions incrementally",[105,2341,2342],{},"Validate condition types early",[105,2344,2345],{},"Test edge cases in dependencies",[19,2347,1119],{"id":1118},[102,2349,2350,2369,2388],{},[105,2351,2352,2355],{},[794,2353,2354],{},"Robustness Improvements",[1070,2356,2357,2360,2363,2366],{},[105,2358,2359],{},"Build type validation system",[105,2361,2362],{},"Add condition testing framework",[105,2364,2365],{},"Implement condition visualization",[105,2367,2368],{},"Create condition templates",[105,2370,2371,2374],{},[794,2372,2373],{},"Developer Experience",[1070,2375,2376,2379,2382,2385],{},[105,2377,2378],{},"Build condition builder UI",[105,2380,2381],{},"Add type generation tools",[105,2383,2384],{},"Improve error messages",[105,2386,2387],{},"Create debugging tools",[105,2389,2390,2393],{},[794,2391,2392],{},"Performance Optimization",[1070,2394,2395,2398,2401,2404],{},[105,2396,2397],{},"Cache field references",[105,2399,2400],{},"Batch condition updates",[105,2402,2403],{},"Optimize condition evaluation",[105,2405,2406],{},"Implement change tracking",[19,2408,1129],{"id":1128},[15,2410,2411,2412,46],{},"In this project what I took away is the importance of balancing technical debt with delivery speed. By leveraging LLMs for type analysis and focusing on core functionality, I was able to deliever a complex form build under tight time constraints ",[83,2413,2414],{},"(quicker than our CSM team could have done it manually)",[15,2416,2417],{},"Hey, sometimes the \"perfect\" solution isn't the right solution. In this case, using AI to understand and generate complex type structures was more efficient than building a comprehensive type system from scratch.",[15,2419,2420],{},"I think it demonstrated how modern development tools (like LLMs) can be creatively applied to solve traditional development challenges, especially when time is a critical factor.",[1154,2422],{},[15,2424,2425],{},[83,2426,2427],{},"Note: This case study focuses on the technical implementation while respecting confidentiality around specific requirements and implementations.",[1162,2429,2430],{},"html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sucvu, html code.shiki .sucvu{--shiki-light:#E53935;--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sfo-9, html code.shiki .sfo-9{--shiki-light:#90A4AE;--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":48,"searchDepth":49,"depth":49,"links":2432},[2433,2434,2435,2439,2440,2441],{"id":77,"depth":49,"text":78},{"id":125,"depth":49,"text":126},{"id":1239,"depth":49,"text":1240,"children":2436},[2437,2438],{"id":1243,"depth":216,"text":1244},{"id":1272,"depth":216,"text":1273},{"id":1047,"depth":49,"text":1048},{"id":1118,"depth":49,"text":1119},{"id":1128,"depth":49,"text":1129},"2025-05-25","Developed an automated system to generate complex conditional form workflows for a Defense Supplier Checklist, leveraging LLMs for type analysis and rapid development.",{"slug":2445},"conditional-form-builder-automation","\u002Fwork\u002Fconditional-form-builder-automation",{"title":1196,"description":2443},"work\u002Fconditional-form-builder-automation",[64,1190,2450,1262,1189],"Form Builder","gn8LW4xk0hB8FrCK63n4atO6cejqRgbDoTeCANiXd6Y",{"id":2453,"title":2454,"body":2455,"date":3935,"description":3936,"extension":55,"externalUrl":1180,"featured":1181,"kind":1182,"meta":3937,"navigation":56,"path":3939,"seo":3940,"stem":3941,"tags":3942,"__hash__":3948},"work\u002Fwork\u002Fenterprise-data-migration-excel-to-platform.md","Excel to web: government infrastructure migration",{"type":8,"value":2456,"toc":3907},[2457,2461,2463,2466,2469,2476,2478,2481,2517,2519,2523,2527,2530,2544,2547,2730,2734,2737,2769,2774,2823,2827,2949,2953,3203,3205,3209,3213,3216,3231,3235,3238,3268,3272,3367,3371,3477,3481,3590,3592,3596,3600,3603,3617,3621,3667,3669,3673,3677,3680,3691,3695,3771,3773,3777,3781,3784,3795,3799,3802,3816,3820,3823,3834,3837,3839,3843,3847,3867,3871,3897,3899,3901,3904],[11,2458,2460],{"id":2459},"migrating-a-large-excel-use-case-to-our-platform","Migrating a large excel use-case to our platform",[19,2462,78],{"id":77},[15,2464,2465],{},"A pretty cool moment for our platform was securing a partnership with a major Dutch government agency in the infrastructure sector. The task was large: migrate highly complex compliance data for over 150 of their clients from intricate Excel spreadsheets into our live web application. Each client's data was provided by multiple assessors, included numerous evidence files, and was critical to their operations.",[15,2467,2468],{},"To be honest that was not just a \"technical task\"; it was a high-stakes project to prove our platform's capability, solidify a key partnership, and save hundreds of hours of manual work. The success of this migration would provide mental ease to our team and to our partner. It would also allow them to use our platform with their data - as intended.",[87,2470,2471],{},[15,2472,2473],{},[83,2474,2475],{},"For confidentiality, I’ll refer to the client as the \"Agency,\" but the core challenge is a common one in the enterprise world: bridging the gap between legacy data formats (like Excel) and modern web platforms, at scale.",[19,2477,126],{"id":125},[15,2479,2480],{},"I developed an end-to-end, automated data migration pipeline that could:",[102,2482,2483,2493,2499,2505,2511],{},[105,2484,2485,2488,2489,2492],{},[794,2486,2487],{},"Parse Complex Excel Files:"," Ingest and interpret multi-sheet ",[162,2490,2491],{},".xlsm"," files containing nested data structures and varied formatting.",[105,2494,2495,2498],{},[794,2496,2497],{},"Programmatically Upload Files:"," Reverse-engineer our platform's S3 file upload mechanism to handle evidence files (images, PDFs, etc.) just like a real user would.",[105,2500,2501,2504],{},[794,2502,2503],{},"Automate Form Submissions:"," Use our platform's GraphQL API to create workflows, fill out complex forms, and associate the uploaded evidence.",[105,2506,2507,2510],{},[794,2508,2509],{},"Run in Multiple Environments:"," Operate seamlessly across testing and production environments with different configurations and IDs.",[105,2512,2513,2516],{},[794,2514,2515],{},"Benchmark Performance:"," Track and report on execution time to estimate the total duration for the full-scale migration.",[1154,2518],{},[19,2520,2522],{"id":2521},"deep-dive-parsing-and-mapping-complex-excel-data","Deep Dive: Parsing and Mapping Complex Excel Data",[148,2524,2526],{"id":2525},"the-real-world-data-model","The Real-World Data Model",[15,2528,2529],{},"The Agency provided a directory of folders, each representing a client (or \"team\"). Inside each folder:",[1070,2531,2532,2538],{},[105,2533,2534,2535,2537],{},"A ",[162,2536,2491],{}," Excel file with multiple sheets (\"Home\", \"Data\", \"Import\")",[105,2539,2534,2540,2543],{},[162,2541,2542],{},"Documents\u002F"," subfolder with evidence files (PDFs, images, spreadsheets, etc.)",[15,2545,2546],{},"Each client had three assessors (\"Undertaking\", \"Initial Approval\", \"Oversight\"), each with their own set of answers and evidence. The data model I built looked like this:",[156,2548,2550],{"className":158,"code":2549,"language":160,"meta":48,"style":48},"interface OrganizationData {\n  organizationSlug: string;\n  teams: Team[];\n}\n\ninterface Team {\n  name: string;\n  uniqueId: string;\n  type_of_organization: string;\n  small: boolean;\n  assessors: Assessor[];\n}\n\ninterface Assessor {\n  name: string;\n  evidence: string[];\n  backdatedCreatedAt?: string;\n  formData: any[];\n}\n",[162,2551,2552,2561,2572,2587,2591,2595,2603,2614,2625,2636,2648,2662,2666,2670,2678,2688,2701,2712,2726],{"__ignoreMap":48},[165,2553,2554,2556,2559],{"class":167,"line":168},[165,2555,1290],{"class":171},[165,2557,2558],{"class":1293}," OrganizationData",[165,2560,1297],{"class":194},[165,2562,2563,2566,2568,2570],{"class":167,"line":49},[165,2564,2565],{"class":1321},"  organizationSlug",[165,2567,451],{"class":179},[165,2569,1310],{"class":748},[165,2571,195],{"class":194},[165,2573,2574,2577,2579,2582,2585],{"class":167,"line":216},[165,2575,2576],{"class":1321},"  teams",[165,2578,451],{"class":179},[165,2580,2581],{"class":1293}," Team",[165,2583,2584],{"class":256},"[]",[165,2586,195],{"class":194},[165,2588,2589],{"class":167,"line":237},[165,2590,784],{"class":194},[165,2592,2593],{"class":167,"line":243},[165,2594,240],{"emptyLinePlaceholder":56},[165,2596,2597,2599,2601],{"class":167,"line":249},[165,2598,1290],{"class":171},[165,2600,2581],{"class":1293},[165,2602,1297],{"class":194},[165,2604,2605,2608,2610,2612],{"class":167,"line":295},[165,2606,2607],{"class":1321},"  name",[165,2609,451],{"class":179},[165,2611,1310],{"class":748},[165,2613,195],{"class":194},[165,2615,2616,2619,2621,2623],{"class":167,"line":348},[165,2617,2618],{"class":1321},"  uniqueId",[165,2620,451],{"class":179},[165,2622,1310],{"class":748},[165,2624,195],{"class":194},[165,2626,2627,2630,2632,2634],{"class":167,"line":353},[165,2628,2629],{"class":1321},"  type_of_organization",[165,2631,451],{"class":179},[165,2633,1310],{"class":748},[165,2635,195],{"class":194},[165,2637,2638,2641,2643,2646],{"class":167,"line":373},[165,2639,2640],{"class":1321},"  small",[165,2642,451],{"class":179},[165,2644,2645],{"class":748}," boolean",[165,2647,195],{"class":194},[165,2649,2650,2653,2655,2658,2660],{"class":167,"line":387},[165,2651,2652],{"class":1321},"  assessors",[165,2654,451],{"class":179},[165,2656,2657],{"class":1293}," Assessor",[165,2659,2584],{"class":256},[165,2661,195],{"class":194},[165,2663,2664],{"class":167,"line":401},[165,2665,784],{"class":194},[165,2667,2668],{"class":167,"line":413},[165,2669,240],{"emptyLinePlaceholder":56},[165,2671,2672,2674,2676],{"class":167,"line":421},[165,2673,1290],{"class":171},[165,2675,2657],{"class":1293},[165,2677,1297],{"class":194},[165,2679,2680,2682,2684,2686],{"class":167,"line":426},[165,2681,2607],{"class":1321},[165,2683,451],{"class":179},[165,2685,1310],{"class":748},[165,2687,195],{"class":194},[165,2689,2690,2693,2695,2697,2699],{"class":167,"line":445},[165,2691,2692],{"class":1321},"  evidence",[165,2694,451],{"class":179},[165,2696,1310],{"class":748},[165,2698,2584],{"class":256},[165,2700,195],{"class":194},[165,2702,2703,2706,2708,2710],{"class":167,"line":472},[165,2704,2705],{"class":1321},"  backdatedCreatedAt",[165,2707,2104],{"class":179},[165,2709,1310],{"class":748},[165,2711,195],{"class":194},[165,2713,2714,2717,2719,2722,2724],{"class":167,"line":489},[165,2715,2716],{"class":1321},"  formData",[165,2718,451],{"class":179},[165,2720,2721],{"class":748}," any",[165,2723,2584],{"class":256},[165,2725,195],{"class":194},[165,2727,2728],{"class":167,"line":499},[165,2729,784],{"class":194},[148,2731,2733],{"id":2732},"excel-parsing-edge-cases-and-lessons","Excel Parsing: Edge Cases and Lessons",[15,2735,2736],{},"Parsing the Excel files was far from trivial. Here are some of the real-world challenges I faced:",[1070,2738,2739,2745,2751,2757,2763],{},[105,2740,2741,2744],{},[794,2742,2743],{},"Non-Uniform Sheets:"," Not every file had the same sheet names or structure. Some had missing or extra sheets, requiring defensive code.",[105,2746,2747,2750],{},[794,2748,2749],{},"Merged Cells and Inconsistent Data:"," Some data was in merged cells, or in different columns depending on the client. I had to write logic to detect and adapt to these variations.",[105,2752,2753,2756],{},[794,2754,2755],{},"Dutch Date Formats:"," Dates were in Dutch (e.g., \"12-mrt-24\"). I wrote a custom parser to handle these, including edge cases like multiple dates in a single cell.",[105,2758,2759,2762],{},[794,2760,2761],{},"Reference Number Mapping:"," Each row in the \"Data\" sheet corresponded to a question, identified by a reference number (e.g., \"5.2.4\"). I built a mapping system to find the right row for each reference, even if the order varied.",[105,2764,2765,2768],{},[794,2766,2767],{},"Assessor-Specific Logic:"," The meaning of each column changed depending on the assessor. For \"Undertaking\", a value in column V meant \"present\"; for \"Oversight\", the same column could mean something else, and the value itself might be a code (\"O1\", \"O2\", etc.) that needed to be mapped to a score.",[2770,2771,2773],"h4",{"id":2772},"example-dutch-date-parsing","Example: Dutch Date Parsing",[156,2775,2777],{"className":158,"code":2776,"language":160,"meta":48,"style":48},"function parseDutchDate(dateStr: string): Date | null {\n  \u002F\u002F Handles formats like \"12-mrt-24\" and \"12\u002F13-mrt-24\"\n  \u002F\u002F ... see code for full implementation\n}\n",[162,2778,2779,2809,2814,2819],{"__ignoreMap":48},[165,2780,2781,2784,2787,2789,2792,2794,2796,2798,2800,2802,2804,2807],{"class":167,"line":168},[165,2782,2783],{"class":171},"function",[165,2785,2786],{"class":303}," parseDutchDate",[165,2788,308],{"class":194},[165,2790,2791],{"class":536},"dateStr",[165,2793,451],{"class":179},[165,2795,1310],{"class":748},[165,2797,343],{"class":194},[165,2799,451],{"class":179},[165,2801,911],{"class":1293},[165,2803,2078],{"class":179},[165,2805,2806],{"class":748}," null",[165,2808,1297],{"class":194},[165,2810,2811],{"class":167,"line":49},[165,2812,2813],{"class":233},"  \u002F\u002F Handles formats like \"12-mrt-24\" and \"12\u002F13-mrt-24\"\n",[165,2815,2816],{"class":167,"line":216},[165,2817,2818],{"class":233},"  \u002F\u002F ... see code for full implementation\n",[165,2820,2821],{"class":167,"line":237},[165,2822,784],{"class":194},[2770,2824,2826],{"id":2825},"example-multi-assessor-data-extraction","Example: Multi-Assessor Data Extraction",[156,2828,2830],{"className":158,"code":2829,"language":160,"meta":48,"style":48},"const assessors: Assessor[] = [\n  {\n    name: \"Undertaking\",\n    evidence: evidenceFiles,\n    formData: createAssessorFormData(\n      dataSheet,\n      importSheet,\n      \"Undertaking\",\n      referenceNumbers,\n    ),\n  },\n  \u002F\u002F ... Initial Approval, Oversight\n];\n",[162,2831,2832,2850,2855,2871,2883,2895,2902,2909,2920,2927,2934,2938,2943],{"__ignoreMap":48},[165,2833,2834,2836,2839,2841,2843,2846,2848],{"class":167,"line":168},[165,2835,172],{"class":171},[165,2837,2838],{"class":175}," assessors",[165,2840,451],{"class":179},[165,2842,2657],{"class":1293},[165,2844,2845],{"class":256},"[] ",[165,2847,266],{"class":179},[165,2849,1899],{"class":256},[165,2851,2852],{"class":167,"line":49},[165,2853,2854],{"class":194},"  {\n",[165,2856,2857,2860,2862,2864,2867,2869],{"class":167,"line":216},[165,2858,2859],{"class":307},"    name",[165,2861,451],{"class":194},[165,2863,184],{"class":183},[165,2865,2866],{"class":187},"Undertaking",[165,2868,191],{"class":183},[165,2870,384],{"class":194},[165,2872,2873,2876,2878,2881],{"class":167,"line":237},[165,2874,2875],{"class":307},"    evidence",[165,2877,451],{"class":194},[165,2879,2880],{"class":256}," evidenceFiles",[165,2882,384],{"class":194},[165,2884,2885,2888,2890,2893],{"class":167,"line":243},[165,2886,2887],{"class":307},"    formData",[165,2889,451],{"class":194},[165,2891,2892],{"class":303}," createAssessorFormData",[165,2894,370],{"class":256},[165,2896,2897,2900],{"class":167,"line":249},[165,2898,2899],{"class":256},"      dataSheet",[165,2901,384],{"class":194},[165,2903,2904,2907],{"class":167,"line":295},[165,2905,2906],{"class":256},"      importSheet",[165,2908,384],{"class":194},[165,2910,2911,2914,2916,2918],{"class":167,"line":348},[165,2912,2913],{"class":183},"      \"",[165,2915,2866],{"class":187},[165,2917,191],{"class":183},[165,2919,384],{"class":194},[165,2921,2922,2925],{"class":167,"line":353},[165,2923,2924],{"class":256},"      referenceNumbers",[165,2926,384],{"class":194},[165,2928,2929,2932],{"class":167,"line":373},[165,2930,2931],{"class":256},"    )",[165,2933,384],{"class":194},[165,2935,2936],{"class":167,"line":387},[165,2937,1816],{"class":194},[165,2939,2940],{"class":167,"line":401},[165,2941,2942],{"class":233},"  \u002F\u002F ... Initial Approval, Oversight\n",[165,2944,2945,2947],{"class":167,"line":413},[165,2946,942],{"class":256},[165,2948,195],{"class":194},[2770,2950,2952],{"id":2951},"example-evidence-file-discovery","Example: Evidence File Discovery",[156,2954,2956],{"className":158,"code":2955,"language":160,"meta":48,"style":48},"const evidenceFiles = fs\n  .readdirSync(documentsPath)\n  .filter((file) =>\n    [\n      \".pdf\",\n      \".xlsx\",\n      \".xls\",\n      \".doc\",\n      \".docx\",\n      \".webp\",\n      \".jpg\",\n      \".jpeg\",\n      \".gif\",\n      \".csv\",\n      \".txt\",\n    ].some((ext) => file.endsWith(ext)),\n  )\n  .map((file) => path.join(teamDir, \"Documents\", file));\n",[162,2957,2958,2969,2980,2998,3003,3014,3025,3036,3047,3058,3069,3080,3091,3102,3112,3123,3156,3161],{"__ignoreMap":48},[165,2959,2960,2962,2964,2966],{"class":167,"line":168},[165,2961,172],{"class":171},[165,2963,2880],{"class":175},[165,2965,180],{"class":179},[165,2967,2968],{"class":256}," fs\n",[165,2970,2971,2974,2977],{"class":167,"line":49},[165,2972,2973],{"class":194},"  .",[165,2975,2976],{"class":303},"readdirSync",[165,2978,2979],{"class":256},"(documentsPath)\n",[165,2981,2982,2984,2987,2989,2991,2994,2996],{"class":167,"line":216},[165,2983,2973],{"class":194},[165,2985,2986],{"class":303},"filter",[165,2988,308],{"class":256},[165,2990,308],{"class":194},[165,2992,2993],{"class":536},"file",[165,2995,343],{"class":194},[165,2997,542],{"class":171},[165,2999,3000],{"class":167,"line":237},[165,3001,3002],{"class":256},"    [\n",[165,3004,3005,3007,3010,3012],{"class":167,"line":243},[165,3006,2913],{"class":183},[165,3008,3009],{"class":187},".pdf",[165,3011,191],{"class":183},[165,3013,384],{"class":194},[165,3015,3016,3018,3021,3023],{"class":167,"line":249},[165,3017,2913],{"class":183},[165,3019,3020],{"class":187},".xlsx",[165,3022,191],{"class":183},[165,3024,384],{"class":194},[165,3026,3027,3029,3032,3034],{"class":167,"line":295},[165,3028,2913],{"class":183},[165,3030,3031],{"class":187},".xls",[165,3033,191],{"class":183},[165,3035,384],{"class":194},[165,3037,3038,3040,3043,3045],{"class":167,"line":348},[165,3039,2913],{"class":183},[165,3041,3042],{"class":187},".doc",[165,3044,191],{"class":183},[165,3046,384],{"class":194},[165,3048,3049,3051,3054,3056],{"class":167,"line":353},[165,3050,2913],{"class":183},[165,3052,3053],{"class":187},".docx",[165,3055,191],{"class":183},[165,3057,384],{"class":194},[165,3059,3060,3062,3065,3067],{"class":167,"line":373},[165,3061,2913],{"class":183},[165,3063,3064],{"class":187},".webp",[165,3066,191],{"class":183},[165,3068,384],{"class":194},[165,3070,3071,3073,3076,3078],{"class":167,"line":387},[165,3072,2913],{"class":183},[165,3074,3075],{"class":187},".jpg",[165,3077,191],{"class":183},[165,3079,384],{"class":194},[165,3081,3082,3084,3087,3089],{"class":167,"line":401},[165,3083,2913],{"class":183},[165,3085,3086],{"class":187},".jpeg",[165,3088,191],{"class":183},[165,3090,384],{"class":194},[165,3092,3093,3095,3098,3100],{"class":167,"line":413},[165,3094,2913],{"class":183},[165,3096,3097],{"class":187},".gif",[165,3099,191],{"class":183},[165,3101,384],{"class":194},[165,3103,3104,3106,3108,3110],{"class":167,"line":421},[165,3105,2913],{"class":183},[165,3107,999],{"class":187},[165,3109,191],{"class":183},[165,3111,384],{"class":194},[165,3113,3114,3116,3119,3121],{"class":167,"line":426},[165,3115,2913],{"class":183},[165,3117,3118],{"class":187},".txt",[165,3120,191],{"class":183},[165,3122,384],{"class":194},[165,3124,3125,3127,3129,3132,3134,3136,3139,3141,3143,3146,3148,3151,3154],{"class":167,"line":445},[165,3126,1994],{"class":256},[165,3128,46],{"class":194},[165,3130,3131],{"class":303},"some",[165,3133,308],{"class":256},[165,3135,308],{"class":194},[165,3137,3138],{"class":536},"ext",[165,3140,343],{"class":194},[165,3142,761],{"class":171},[165,3144,3145],{"class":256}," file",[165,3147,46],{"class":194},[165,3149,3150],{"class":303},"endsWith",[165,3152,3153],{"class":256},"(ext))",[165,3155,384],{"class":194},[165,3157,3158],{"class":167,"line":472},[165,3159,3160],{"class":256},"  )\n",[165,3162,3163,3165,3168,3170,3172,3174,3176,3178,3180,3182,3184,3187,3189,3191,3194,3196,3198,3201],{"class":167,"line":489},[165,3164,2973],{"class":194},[165,3166,3167],{"class":303},"map",[165,3169,308],{"class":256},[165,3171,308],{"class":194},[165,3173,2993],{"class":536},[165,3175,343],{"class":194},[165,3177,761],{"class":171},[165,3179,973],{"class":256},[165,3181,46],{"class":194},[165,3183,978],{"class":303},[165,3185,3186],{"class":256},"(teamDir",[165,3188,682],{"class":194},[165,3190,184],{"class":183},[165,3192,3193],{"class":187},"Documents",[165,3195,191],{"class":183},[165,3197,682],{"class":194},[165,3199,3200],{"class":256}," file))",[165,3202,195],{"class":194},[1154,3204],{},[19,3206,3208],{"id":3207},"automating-file-uploads-s3-mime-types-and-error-handling","Automating File Uploads: S3, MIME Types, and Error Handling",[148,3210,3212],{"id":3211},"reverse-engineering-the-upload-flow","Reverse-Engineering the Upload Flow",[15,3214,3215],{},"The platform used a three-step S3 upload process, but there was no documentation. I used browser DevTools to:",[1070,3217,3218,3221,3228],{},[105,3219,3220],{},"Watch the GraphQL mutation that returned a signed S3 URL and required fields",[105,3222,3223,3224,3227],{},"Observe the ",[162,3225,3226],{},"PUT"," request to S3, including required headers and form fields",[105,3229,3230],{},"See the follow-up mutation that finalized the upload",[148,3232,3234],{"id":3233},"handling-mime-types-and-file-validation","Handling MIME Types and File Validation",[15,3236,3237],{},"Uploading files at scale meant handling a wide variety of file types and edge cases:",[1070,3239,3240,3250,3256,3262],{},[105,3241,3242,3245,3246,3249],{},[794,3243,3244],{},"MIME Type Detection:"," I built a function to map file extensions to MIME types, defaulting to ",[162,3247,3248],{},"application\u002Foctet-stream"," for unknowns.",[105,3251,3252,3255],{},[794,3253,3254],{},"File Size Limits:"," The script checked for files over 100MB and skipped or flagged them.",[105,3257,3258,3261],{},[794,3259,3260],{},"Empty Files:"," Zero-byte files were detected and logged.",[105,3263,3264,3267],{},[794,3265,3266],{},"Throttling:"," To avoid rate limits and S3 errors, uploads were throttled with a small delay between each file.",[2770,3269,3271],{"id":3270},"example-mime-type-detection","Example: MIME Type Detection",[156,3273,3275],{"className":158,"code":3274,"language":160,"meta":48,"style":48},"function getMimeType(filePath: string): string {\n  const extension = path.extname(filePath).toLowerCase();\n  \u002F\u002F ... mapping logic\n  return mimeTypes[extension] || \"application\u002Foctet-stream\";\n}\n",[162,3276,3277,3301,3332,3337,3363],{"__ignoreMap":48},[165,3278,3279,3281,3284,3286,3289,3291,3293,3295,3297,3299],{"class":167,"line":168},[165,3280,2783],{"class":171},[165,3282,3283],{"class":303}," getMimeType",[165,3285,308],{"class":194},[165,3287,3288],{"class":536},"filePath",[165,3290,451],{"class":179},[165,3292,1310],{"class":748},[165,3294,343],{"class":194},[165,3296,451],{"class":179},[165,3298,1310],{"class":748},[165,3300,1297],{"class":194},[165,3302,3303,3305,3308,3310,3312,3314,3317,3319,3321,3323,3325,3328,3330],{"class":167,"line":49},[165,3304,356],{"class":171},[165,3306,3307],{"class":175}," extension",[165,3309,180],{"class":179},[165,3311,973],{"class":256},[165,3313,46],{"class":194},[165,3315,3316],{"class":303},"extname",[165,3318,308],{"class":307},[165,3320,3288],{"class":256},[165,3322,343],{"class":307},[165,3324,46],{"class":194},[165,3326,3327],{"class":303},"toLowerCase",[165,3329,914],{"class":307},[165,3331,195],{"class":194},[165,3333,3334],{"class":167,"line":216},[165,3335,3336],{"class":233},"  \u002F\u002F ... mapping logic\n",[165,3338,3339,3342,3345,3347,3350,3352,3355,3357,3359,3361],{"class":167,"line":237},[165,3340,3341],{"class":252},"  return",[165,3343,3344],{"class":256}," mimeTypes",[165,3346,935],{"class":307},[165,3348,3349],{"class":256},"extension",[165,3351,1475],{"class":307},[165,3353,3354],{"class":179},"||",[165,3356,184],{"class":183},[165,3358,3248],{"class":187},[165,3360,191],{"class":183},[165,3362,195],{"class":194},[165,3364,3365],{"class":167,"line":243},[165,3366,784],{"class":194},[2770,3368,3370],{"id":3369},"example-throttled-uploads","Example: Throttled Uploads",[156,3372,3374],{"className":158,"code":3373,"language":160,"meta":48,"style":48},"async function throttledUpload(\n  file: string,\n  delayMs: number = 10,\n): Promise\u003Cany> {\n  \u002F\u002F ... upload logic\n  await new Promise((resolve) => setTimeout(resolve, delayMs));\n}\n",[162,3375,3376,3389,3400,3417,3435,3440,3473],{"__ignoreMap":48},[165,3377,3378,3381,3384,3387],{"class":167,"line":168},[165,3379,3380],{"class":171},"async",[165,3382,3383],{"class":171}," function",[165,3385,3386],{"class":303}," throttledUpload",[165,3388,370],{"class":194},[165,3390,3391,3394,3396,3398],{"class":167,"line":49},[165,3392,3393],{"class":536},"  file",[165,3395,451],{"class":179},[165,3397,1310],{"class":748},[165,3399,384],{"class":194},[165,3401,3402,3405,3407,3410,3412,3415],{"class":167,"line":216},[165,3403,3404],{"class":536},"  delayMs",[165,3406,451],{"class":179},[165,3408,3409],{"class":748}," number",[165,3411,180],{"class":179},[165,3413,3414],{"class":226}," 10",[165,3416,384],{"class":194},[165,3418,3419,3421,3423,3425,3427,3430,3433],{"class":167,"line":237},[165,3420,343],{"class":194},[165,3422,451],{"class":179},[165,3424,749],{"class":1293},[165,3426,276],{"class":194},[165,3428,3429],{"class":748},"any",[165,3431,3432],{"class":194},">",[165,3434,1297],{"class":194},[165,3436,3437],{"class":167,"line":243},[165,3438,3439],{"class":233},"  \u002F\u002F ... upload logic\n",[165,3441,3442,3444,3446,3448,3450,3452,3454,3456,3458,3460,3462,3464,3466,3469,3471],{"class":167,"line":249},[165,3443,742],{"class":252},[165,3445,745],{"class":179},[165,3447,749],{"class":748},[165,3449,308],{"class":307},[165,3451,308],{"class":194},[165,3453,756],{"class":536},[165,3455,343],{"class":194},[165,3457,761],{"class":171},[165,3459,764],{"class":303},[165,3461,308],{"class":307},[165,3463,756],{"class":256},[165,3465,682],{"class":194},[165,3467,3468],{"class":256}," delayMs",[165,3470,776],{"class":307},[165,3472,195],{"class":194},[165,3474,3475],{"class":167,"line":295},[165,3476,784],{"class":194},[2770,3478,3480],{"id":3479},"example-error-handling","Example: Error Handling",[156,3482,3484],{"className":158,"code":3483,"language":160,"meta":48,"style":48},"try {\n  await fs.promises.access(filePath, fs.constants.R_OK);\n} catch (error) {\n  throw new Error(`File not accessible: ${filePath}. Error: ${error.message}`);\n}\n",[162,3485,3486,3493,3532,3544,3586],{"__ignoreMap":48},[165,3487,3488,3491],{"class":167,"line":168},[165,3489,3490],{"class":252},"try",[165,3492,1297],{"class":194},[165,3494,3495,3497,3500,3502,3505,3507,3510,3512,3514,3516,3518,3520,3523,3525,3528,3530],{"class":167,"line":49},[165,3496,742],{"class":252},[165,3498,3499],{"class":256}," fs",[165,3501,46],{"class":194},[165,3503,3504],{"class":256},"promises",[165,3506,46],{"class":194},[165,3508,3509],{"class":303},"access",[165,3511,308],{"class":307},[165,3513,3288],{"class":256},[165,3515,682],{"class":194},[165,3517,3499],{"class":256},[165,3519,46],{"class":194},[165,3521,3522],{"class":256},"constants",[165,3524,46],{"class":194},[165,3526,3527],{"class":175},"R_OK",[165,3529,343],{"class":307},[165,3531,195],{"class":194},[165,3533,3534,3536,3539,3542],{"class":167,"line":216},[165,3535,329],{"class":194},[165,3537,3538],{"class":252}," catch",[165,3540,3541],{"class":256}," (error) ",[165,3543,292],{"class":194},[165,3545,3546,3549,3551,3554,3556,3558,3561,3563,3565,3567,3570,3572,3575,3577,3580,3582,3584],{"class":167,"line":237},[165,3547,3548],{"class":252},"  throw",[165,3550,745],{"class":179},[165,3552,3553],{"class":303}," Error",[165,3555,308],{"class":307},[165,3557,311],{"class":183},[165,3559,3560],{"class":187},"File not accessible: ",[165,3562,317],{"class":183},[165,3564,3288],{"class":256},[165,3566,329],{"class":183},[165,3568,3569],{"class":187},". Error: ",[165,3571,317],{"class":183},[165,3573,3574],{"class":256},"error",[165,3576,46],{"class":183},[165,3578,3579],{"class":256},"message",[165,3581,340],{"class":183},[165,3583,343],{"class":307},[165,3585,195],{"class":194},[165,3587,3588],{"class":167,"line":243},[165,3589,784],{"class":194},[1154,3591],{},[19,3593,3595],{"id":3594},"dynamic-multi-environment-automation","Dynamic, Multi-Environment Automation",[148,3597,3599],{"id":3598},"test-vs-production-no-hardcoding-allowed","Test vs. Production: No Hardcoding Allowed",[15,3601,3602],{},"The migration had to run on both a test account and the real production environment. Each had different tenant IDs, workflow IDs, and team IDs. To make the script portable:",[1070,3604,3605,3608,3614],{},[105,3606,3607],{},"All IDs were looked up dynamically by name or slug at runtime",[105,3609,3610,3613],{},[162,3611,3612],{},".env"," files were used for environment-specific configuration",[105,3615,3616],{},"The script could be pointed at any organization, workflow, or team with a simple config change",[2770,3618,3620],{"id":3619},"example-dynamic-workflow-lookup","Example: Dynamic Workflow Lookup",[156,3622,3624],{"className":158,"code":3623,"language":160,"meta":48,"style":48},"const availableWorkflows = await availableWorkflowsForUser(tenantId);\nconst workflowCollectionId = availableWorkflows[0].workflowCollectionId;\n",[162,3625,3626,3645],{"__ignoreMap":48},[165,3627,3628,3630,3633,3635,3637,3640,3643],{"class":167,"line":168},[165,3629,172],{"class":171},[165,3631,3632],{"class":175}," availableWorkflows",[165,3634,180],{"class":179},[165,3636,364],{"class":252},[165,3638,3639],{"class":303}," availableWorkflowsForUser",[165,3641,3642],{"class":256},"(tenantId)",[165,3644,195],{"class":194},[165,3646,3647,3649,3652,3654,3657,3659,3661,3663,3665],{"class":167,"line":49},[165,3648,172],{"class":171},[165,3650,3651],{"class":175}," workflowCollectionId",[165,3653,180],{"class":179},[165,3655,3656],{"class":256}," availableWorkflows[",[165,3658,1692],{"class":226},[165,3660,942],{"class":256},[165,3662,46],{"class":194},[165,3664,396],{"class":256},[165,3666,195],{"class":194},[1154,3668],{},[19,3670,3672],{"id":3671},"performance-benchmarking-and-reporting","Performance Benchmarking and Reporting",[148,3674,3676],{"id":3675},"why-it-mattered","Why It Mattered",[15,3678,3679],{},"With hundreds of clients and thousands of files, knowing how long the migration would take was critical for planning and client communication. I built a timing metrics module to:",[1070,3681,3682,3685,3688],{},[105,3683,3684],{},"Track start and end times for each team and assessor",[105,3686,3687],{},"Calculate average time per client",[105,3689,3690],{},"Output a detailed timing report as JSON",[2770,3692,3694],{"id":3693},"example-timing-metrics","Example: Timing Metrics",[156,3696,3698],{"className":158,"code":3697,"language":160,"meta":48,"style":48},"const metrics = {\n  startTime: Date.now(),\n  clientsProcessed: 0,\n  clientTimings: [],\n  endTime: null,\n};\n\u002F\u002F ... see code for full implementation\n",[162,3699,3700,3711,3728,3739,3751,3762,3766],{"__ignoreMap":48},[165,3701,3702,3704,3707,3709],{"class":167,"line":168},[165,3703,172],{"class":171},[165,3705,3706],{"class":175}," metrics",[165,3708,180],{"class":179},[165,3710,1297],{"class":194},[165,3712,3713,3716,3718,3720,3722,3724,3726],{"class":167,"line":49},[165,3714,3715],{"class":307},"  startTime",[165,3717,451],{"class":194},[165,3719,911],{"class":256},[165,3721,46],{"class":194},[165,3723,85],{"class":303},[165,3725,914],{"class":256},[165,3727,384],{"class":194},[165,3729,3730,3733,3735,3737],{"class":167,"line":216},[165,3731,3732],{"class":307},"  clientsProcessed",[165,3734,451],{"class":194},[165,3736,269],{"class":226},[165,3738,384],{"class":194},[165,3740,3741,3744,3746,3749],{"class":167,"line":237},[165,3742,3743],{"class":307},"  clientTimings",[165,3745,451],{"class":194},[165,3747,3748],{"class":256}," []",[165,3750,384],{"class":194},[165,3752,3753,3756,3758,3760],{"class":167,"line":243},[165,3754,3755],{"class":307},"  endTime",[165,3757,451],{"class":194},[165,3759,2806],{"class":934},[165,3761,384],{"class":194},[165,3763,3764],{"class":167,"line":249},[165,3765,1515],{"class":194},[165,3767,3768],{"class":167,"line":295},[165,3769,3770],{"class":233},"\u002F\u002F ... see code for full implementation\n",[1154,3772],{},[19,3774,3776],{"id":3775},"lessons-learned-requirements-communication-and-scope","Lessons Learned: Requirements, Communication, and Scope",[148,3778,3780],{"id":3779},"the-hidden-complexity-of-obvious-requirements","The Hidden Complexity of \"Obvious\" Requirements",[15,3782,3783],{},"One of the biggest surprises was how much detail was hidden in the \"obvious\" parts of the requirements. For example:",[1070,3785,3786,3789,3792],{},[105,3787,3788],{},"The CSM team would describe a field as \"just a date\", but the Excel files had multiple date formats, sometimes in Dutch, sometimes with multiple dates in one cell.",[105,3790,3791],{},"The mapping between Excel columns and form fields was not always 1:1; sometimes it depended on the assessor, or on the value of another cell.",[105,3793,3794],{},"Some evidence files were missing, duplicated, or in unexpected formats.",[148,3796,3798],{"id":3797},"the-importance-of-asking-and-re-asking-questions","The Importance of Asking (and Re-asking) Questions",[15,3800,3801],{},"I learned to:",[1070,3803,3804,3807,3810,3813],{},[105,3805,3806],{},"Never assume the requirements are fully known up front",[105,3808,3809],{},"Ask for sample data and edge cases",[105,3811,3812],{},"Walk through the process with the CSM team, step by step",[105,3814,3815],{},"Document every mapping and transformation rule",[148,3817,3819],{"id":3818},"scope-creep-and-change-management","Scope Creep and Change Management",[15,3821,3822],{},"Even after the initial migration logic was built, new requirements kept emerging:",[1070,3824,3825,3828,3831],{},[105,3826,3827],{},"\"Can we add a new field for improvement suggestions?\"",[105,3829,3830],{},"\"Some clients have more than three assessors, can we support that?\"",[105,3832,3833],{},"\"We need to backdate the creation date for Oversight submissions.\"",[15,3835,3836],{},"I built the script to be as flexible as possible, but also learned to communicate clearly about what changes would require rework.",[1154,3838],{},[19,3840,3842],{"id":3841},"real-world-impact-and-next-steps","Real-World Impact and Next Steps",[148,3844,3846],{"id":3845},"what-this-enabled","What This Enabled",[1070,3848,3849,3855,3861],{},[105,3850,3851,3854],{},[794,3852,3853],{},"Massive Time Savings:"," What would have taken weeks of manual data entry was completed in hours.",[105,3856,3857,3860],{},[794,3858,3859],{},"Data Quality:"," Automated validation and error logging caught issues that would have been missed by hand.",[105,3862,3863,3866],{},[794,3864,3865],{},"Repeatability:"," The script can be re-run for new data drops, or adapted for other clients with similar needs.",[148,3868,3870],{"id":3869},"what-id-improve-next","What I'd Improve Next",[1070,3872,3873,3879,3885,3891],{},[105,3874,3875,3878],{},[794,3876,3877],{},"Dry Run Mode:"," Simulate the migration and output a report of what would be changed, without making any API calls.",[105,3880,3881,3884],{},[794,3882,3883],{},"Better Error Reporting:"," Aggregate errors and warnings into a single report for easier review.",[105,3886,3887,3890],{},[794,3888,3889],{},"UI for CSMs:"," Build a simple web interface so non-engineers can run migrations themselves.",[105,3892,3893,3896],{},[794,3894,3895],{},"Parallelization:"," Carefully parallelize uploads and submissions to further speed up the process, while respecting rate limits.",[1154,3898],{},[19,3900,1129],{"id":1128},[15,3902,3903],{},"This project helped me understand a handful of realities regarding enterprise-scale data migration. It was a mix of meticulous planning, technical reverse-engineering, and constant communication. It showed me that automation, even in the form of a purpose-built script, can provide immense value, strengthen partnerships, and serve as a powerful sales and retention tool. Also for sure showed me that sometimes the most complex technical challenges are best solved by first asking the right human questions.",[1162,3905,3906],{},"html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sucvu, html code.shiki .sucvu{--shiki-light:#E53935;--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s39Yj, html code.shiki .s39Yj{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":48,"searchDepth":49,"depth":49,"links":3908},[3909,3910,3911,3915,3919,3922,3925,3930,3934],{"id":77,"depth":49,"text":78},{"id":125,"depth":49,"text":126},{"id":2521,"depth":49,"text":2522,"children":3912},[3913,3914],{"id":2525,"depth":216,"text":2526},{"id":2732,"depth":216,"text":2733},{"id":3207,"depth":49,"text":3208,"children":3916},[3917,3918],{"id":3211,"depth":216,"text":3212},{"id":3233,"depth":216,"text":3234},{"id":3594,"depth":49,"text":3595,"children":3920},[3921],{"id":3598,"depth":216,"text":3599},{"id":3671,"depth":49,"text":3672,"children":3923},[3924],{"id":3675,"depth":216,"text":3676},{"id":3775,"depth":49,"text":3776,"children":3926},[3927,3928,3929],{"id":3779,"depth":216,"text":3780},{"id":3797,"depth":216,"text":3798},{"id":3818,"depth":216,"text":3819},{"id":3841,"depth":49,"text":3842,"children":3931},[3932,3933],{"id":3845,"depth":216,"text":3846},{"id":3869,"depth":216,"text":3870},{"id":1128,"depth":49,"text":1129},"2025-04-30","Engineered a large-scale data migration pipeline for a Dutch government client, automating the transfer of complex Excel data and file uploads into a live web application by reverse-engineering the platform's S3 upload mechanism.",{"slug":3938},"enterprise-data-migration-excel-to-platform","\u002Fwork\u002Fenterprise-data-migration-excel-to-platform",{"title":2454,"description":3936},"work\u002Fenterprise-data-migration-excel-to-platform",[64,3943,3944,3945,3946,3947,1190,1189],"Node.js","ETL","Data Migration","Excel","S3","jFCmAeFmTpa-uVtx-jARI_ZexTr9lm7o4JxkK9eE6Ow",{"id":3950,"title":3951,"body":3952,"date":4965,"description":4966,"extension":55,"externalUrl":1180,"featured":1181,"kind":1182,"meta":4967,"navigation":56,"path":4968,"seo":4969,"stem":4970,"tags":4971,"__hash__":4974},"work\u002Fwork\u002Fproject-dataharvester-etl-pipeline.md","Real estate data pipeline: MLS integration",{"type":8,"value":3953,"toc":4953},[3954,3958,3960,3963,3970,3973,3975,3978,3992,3995,3997,3999,4023,4025,4032,4190,4193,4204,4207,4214,4399,4402,4413,4420,4631,4634,4638,4670,4672,4679,4682,4685,4692,4726,4729,4736,4739,4746,4862,4865,4867,4932,4934,4937,4940,4943,4945,4950],[11,3955,3957],{"id":3956},"building-a-scalable-real-estate-data-etl-pipeline","Building a scalable real estate data ETL pipeline",[19,3959,78],{"id":77},[15,3961,3962],{},"While working with a government agency client, I faced an interesting data engineering challenge. They needed to collect and analyze comprehensive data about real estate agencies and their properties from a proprietary Multiple Listing Service (MLS) system. The scope? Hundreds of thousands of property records that needed to be extracted, transformed, and analyzed while carefully respecting API rate limits and handling potential failures.",[87,3964,3965],{},[15,3966,3967],{},[83,3968,3969],{},"Note: Out of respect for API terms and client confidentiality, I've anonymized the specific platform and client details. However, the technical challenge is common in enterprise data integration: working with rate-limited APIs, handling large datasets efficiently, and maintaining data integrity throughout the ETL process.",[15,3971,3972],{},"What made this particularly interesting was the balance between aggressive data collection and being a good API citizen – I needed to get this done efficiently but without overwhelming the source system.",[19,3974,126],{"id":125},[15,3976,3977],{},"I developed a resilient ETL (Extract, Transform, Load) pipeline that could:",[102,3979,3980,3983,3986,3989],{},[105,3981,3982],{},"Extract property data from the MLS API in configurable batches",[105,3984,3985],{},"Process and deduplicate agency information on the fly",[105,3987,3988],{},"Transform the raw data into clean, analysis-ready CSV format",[105,3990,3991],{},"Handle interruptions gracefully with built-in checkpointing",[15,3993,3994],{},"The system was designed to be both robust and considerate – implementing smart rate limiting, token management, and data integrity checks throughout the pipeline.",[19,3996,1240],{"id":1239},[148,3998,1244],{"id":1243},[1070,4000,4001,4005,4011,4017],{},[105,4002,4003,1252],{},[794,4004,1251],{},[105,4006,4007,4010],{},[794,4008,4009],{},"Axios",": HTTP client for API interactions",[105,4012,4013,4016],{},[794,4014,4015],{},"File System (fs)",": For data persistence and checkpointing",[105,4018,4019,4022],{},[794,4020,4021],{},"Path",": Cross-platform file path handling",[148,4024,1273],{"id":1272},[102,4026,4027],{},[105,4028,4029],{},[794,4030,4031],{},"Chunked Data Processing",[156,4033,4035],{"className":158,"code":4034,"language":160,"meta":48,"style":48},"const batchSize = 100;\nconst maxPropertiesPerFile = 70000;\n\nasync function fetchAllProperties() {\n  \u002F\u002F Create output directory if it doesn't exist\n  if (!fs.existsSync(outputDir)) {\n    fs.mkdirSync(outputDir);\n  }\n\n  \u002F\u002F Find the last file I was working on\n  let currentFileIndex = 0;\n  let allProperties: any[] = [];\n\n  \u002F\u002F ... chunked processing logic\n}\n",[162,4036,4037,4051,4065,4069,4082,4087,4114,4131,4135,4139,4144,4158,4177,4181,4186],{"__ignoreMap":48},[165,4038,4039,4041,4044,4046,4049],{"class":167,"line":168},[165,4040,172],{"class":171},[165,4042,4043],{"class":175}," batchSize",[165,4045,180],{"class":179},[165,4047,4048],{"class":226}," 100",[165,4050,195],{"class":194},[165,4052,4053,4055,4058,4060,4063],{"class":167,"line":49},[165,4054,172],{"class":171},[165,4056,4057],{"class":175}," maxPropertiesPerFile",[165,4059,180],{"class":179},[165,4061,4062],{"class":226}," 70000",[165,4064,195],{"class":194},[165,4066,4067],{"class":167,"line":216},[165,4068,240],{"emptyLinePlaceholder":56},[165,4070,4071,4073,4075,4078,4080],{"class":167,"line":237},[165,4072,3380],{"class":171},[165,4074,3383],{"class":171},[165,4076,4077],{"class":303}," fetchAllProperties",[165,4079,914],{"class":194},[165,4081,1297],{"class":194},[165,4083,4084],{"class":167,"line":243},[165,4085,4086],{"class":233},"  \u002F\u002F Create output directory if it doesn't exist\n",[165,4088,4089,4091,4093,4096,4099,4101,4104,4106,4109,4112],{"class":167,"line":249},[165,4090,589],{"class":252},[165,4092,257],{"class":307},[165,4094,4095],{"class":179},"!",[165,4097,4098],{"class":256},"fs",[165,4100,46],{"class":194},[165,4102,4103],{"class":303},"existsSync",[165,4105,308],{"class":307},[165,4107,4108],{"class":256},"outputDir",[165,4110,4111],{"class":307},")) ",[165,4113,292],{"class":194},[165,4115,4116,4118,4120,4123,4125,4127,4129],{"class":167,"line":295},[165,4117,701],{"class":256},[165,4119,46],{"class":194},[165,4121,4122],{"class":303},"mkdirSync",[165,4124,308],{"class":307},[165,4126,4108],{"class":256},[165,4128,343],{"class":307},[165,4130,195],{"class":194},[165,4132,4133],{"class":167,"line":348},[165,4134,725],{"class":194},[165,4136,4137],{"class":167,"line":353},[165,4138,240],{"emptyLinePlaceholder":56},[165,4140,4141],{"class":167,"line":373},[165,4142,4143],{"class":233},"  \u002F\u002F Find the last file I was working on\n",[165,4145,4146,4149,4152,4154,4156],{"class":167,"line":387},[165,4147,4148],{"class":171},"  let",[165,4150,4151],{"class":256}," currentFileIndex",[165,4153,180],{"class":179},[165,4155,269],{"class":226},[165,4157,195],{"class":194},[165,4159,4160,4162,4165,4167,4169,4171,4173,4175],{"class":167,"line":401},[165,4161,4148],{"class":171},[165,4163,4164],{"class":256}," allProperties",[165,4166,451],{"class":179},[165,4168,2721],{"class":748},[165,4170,2845],{"class":307},[165,4172,266],{"class":179},[165,4174,3748],{"class":307},[165,4176,195],{"class":194},[165,4178,4179],{"class":167,"line":413},[165,4180,240],{"emptyLinePlaceholder":56},[165,4182,4183],{"class":167,"line":421},[165,4184,4185],{"class":233},"  \u002F\u002F ... chunked processing logic\n",[165,4187,4188],{"class":167,"line":426},[165,4189,784],{"class":194},[15,4191,4192],{},"I implemented a chunked processing system that:",[1070,4194,4195,4198,4201],{},[105,4196,4197],{},"Fetches data in small batches (100 properties)",[105,4199,4200],{},"Splits output into manageable files (~70K properties each)",[105,4202,4203],{},"Enables resume-ability if the process fails",[15,4205,4206],{},"This approach proved crucial when dealing with the full dataset of nearly 700,000 properties.",[102,4208,4209],{"start":49},[105,4210,4211],{},[794,4212,4213],{},"Resilient Error Handling",[156,4215,4217],{"className":158,"code":4216,"language":160,"meta":48,"style":48},"try {\n  const response = await axios.post\u003CSearchResponse>(\n    API_ENDPOINT,\n    requestPayload,\n    { headers },\n  );\n} catch (error: any) {\n  if (error?.response?.status === 401) {\n    console.error(\n      \"Token expired! Please update the authorization token and run again\",\n    );\n    return;\n  }\n  \u002F\u002F For other errors, wait and retry\n  await delay(5000);\n  continue;\n}\n",[162,4218,4219,4225,4253,4260,4267,4277,4283,4301,4328,4339,4350,4356,4363,4367,4372,4388,4395],{"__ignoreMap":48},[165,4220,4221,4223],{"class":167,"line":168},[165,4222,3490],{"class":252},[165,4224,1297],{"class":194},[165,4226,4227,4229,4232,4234,4236,4239,4241,4244,4246,4249,4251],{"class":167,"line":49},[165,4228,356],{"class":171},[165,4230,4231],{"class":175}," response",[165,4233,180],{"class":179},[165,4235,364],{"class":252},[165,4237,4238],{"class":256}," axios",[165,4240,46],{"class":194},[165,4242,4243],{"class":303},"post",[165,4245,276],{"class":194},[165,4247,4248],{"class":1293},"SearchResponse",[165,4250,3432],{"class":194},[165,4252,370],{"class":307},[165,4254,4255,4258],{"class":167,"line":216},[165,4256,4257],{"class":175},"    API_ENDPOINT",[165,4259,384],{"class":194},[165,4261,4262,4265],{"class":167,"line":237},[165,4263,4264],{"class":256},"    requestPayload",[165,4266,384],{"class":194},[165,4268,4269,4272,4275],{"class":167,"line":243},[165,4270,4271],{"class":194},"    {",[165,4273,4274],{"class":256}," headers",[165,4276,1931],{"class":194},[165,4278,4279,4281],{"class":167,"line":249},[165,4280,416],{"class":307},[165,4282,195],{"class":194},[165,4284,4285,4287,4289,4291,4293,4295,4297,4299],{"class":167,"line":295},[165,4286,329],{"class":194},[165,4288,3538],{"class":252},[165,4290,257],{"class":194},[165,4292,3574],{"class":536},[165,4294,451],{"class":179},[165,4296,2721],{"class":748},[165,4298,343],{"class":194},[165,4300,1297],{"class":194},[165,4302,4303,4305,4307,4309,4311,4314,4316,4318,4321,4324,4326],{"class":167,"line":348},[165,4304,589],{"class":252},[165,4306,257],{"class":307},[165,4308,3574],{"class":256},[165,4310,393],{"class":194},[165,4312,4313],{"class":256},"response",[165,4315,393],{"class":194},[165,4317,28],{"class":256},[165,4319,4320],{"class":179}," ===",[165,4322,4323],{"class":226}," 401",[165,4325,289],{"class":307},[165,4327,292],{"class":194},[165,4329,4330,4333,4335,4337],{"class":167,"line":353},[165,4331,4332],{"class":256},"    console",[165,4334,46],{"class":194},[165,4336,3574],{"class":303},[165,4338,370],{"class":307},[165,4340,4341,4343,4346,4348],{"class":167,"line":373},[165,4342,2913],{"class":183},[165,4344,4345],{"class":187},"Token expired! Please update the authorization token and run again",[165,4347,191],{"class":183},[165,4349,384],{"class":194},[165,4351,4352,4354],{"class":167,"line":387},[165,4353,2931],{"class":307},[165,4355,195],{"class":194},[165,4357,4358,4361],{"class":167,"line":401},[165,4359,4360],{"class":252},"    return",[165,4362,195],{"class":194},[165,4364,4365],{"class":167,"line":413},[165,4366,725],{"class":194},[165,4368,4369],{"class":167,"line":421},[165,4370,4371],{"class":233},"  \u002F\u002F For other errors, wait and retry\n",[165,4373,4374,4376,4379,4381,4384,4386],{"class":167,"line":426},[165,4375,742],{"class":252},[165,4377,4378],{"class":303}," delay",[165,4380,308],{"class":307},[165,4382,4383],{"class":226},"5000",[165,4385,343],{"class":307},[165,4387,195],{"class":194},[165,4389,4390,4393],{"class":167,"line":445},[165,4391,4392],{"class":252},"  continue",[165,4394,195],{"class":194},[165,4396,4397],{"class":167,"line":472},[165,4398,784],{"class":194},[15,4400,4401],{},"The system handles:",[1070,4403,4404,4407,4410],{},[105,4405,4406],{},"Token expiration gracefully",[105,4408,4409],{},"Network failures with automatic retries",[105,4411,4412],{},"Rate limiting through intelligent delays",[102,4414,4415],{"start":216},[105,4416,4417],{},[794,4418,4419],{},"Smart Deduplication",[156,4421,4423],{"className":158,"code":4422,"language":160,"meta":48,"style":48},"const agencyMap = new Map\u003Cstring, Agency>();\n\n\u002F\u002F During property processing\nproperties.forEach((property) => {\n  const { agency } = property;\n  if (agency?.email) {\n    agencyMap.set(agency.email, {\n      email: agency.email,\n      name: agency.name,\n      phone: agency.phone,\n      websiteUrl: agency.websiteUrl,\n    });\n  }\n});\n",[162,4424,4425,4455,4459,4464,4487,4507,4525,4547,4562,4578,4594,4610,4619,4623],{"__ignoreMap":48},[165,4426,4427,4429,4432,4434,4436,4439,4441,4444,4446,4449,4451,4453],{"class":167,"line":168},[165,4428,172],{"class":171},[165,4430,4431],{"class":175}," agencyMap",[165,4433,180],{"class":179},[165,4435,745],{"class":179},[165,4437,4438],{"class":303}," Map",[165,4440,276],{"class":194},[165,4442,4443],{"class":748},"string",[165,4445,682],{"class":194},[165,4447,4448],{"class":1293}," Agency",[165,4450,3432],{"class":194},[165,4452,914],{"class":256},[165,4454,195],{"class":194},[165,4456,4457],{"class":167,"line":49},[165,4458,240],{"emptyLinePlaceholder":56},[165,4460,4461],{"class":167,"line":216},[165,4462,4463],{"class":233},"\u002F\u002F During property processing\n",[165,4465,4466,4469,4471,4474,4476,4478,4481,4483,4485],{"class":167,"line":237},[165,4467,4468],{"class":256},"properties",[165,4470,46],{"class":194},[165,4472,4473],{"class":303},"forEach",[165,4475,308],{"class":256},[165,4477,308],{"class":194},[165,4479,4480],{"class":536},"property",[165,4482,343],{"class":194},[165,4484,761],{"class":171},[165,4486,1297],{"class":194},[165,4488,4489,4491,4494,4497,4500,4502,4505],{"class":167,"line":243},[165,4490,356],{"class":171},[165,4492,4493],{"class":194}," {",[165,4495,4496],{"class":175}," agency",[165,4498,4499],{"class":194}," }",[165,4501,180],{"class":179},[165,4503,4504],{"class":256}," property",[165,4506,195],{"class":194},[165,4508,4509,4511,4513,4516,4518,4521,4523],{"class":167,"line":249},[165,4510,589],{"class":252},[165,4512,257],{"class":307},[165,4514,4515],{"class":256},"agency",[165,4517,393],{"class":194},[165,4519,4520],{"class":256},"email",[165,4522,289],{"class":307},[165,4524,292],{"class":194},[165,4526,4527,4530,4532,4535,4537,4539,4541,4543,4545],{"class":167,"line":295},[165,4528,4529],{"class":256},"    agencyMap",[165,4531,46],{"class":194},[165,4533,4534],{"class":303},"set",[165,4536,308],{"class":307},[165,4538,4515],{"class":256},[165,4540,46],{"class":194},[165,4542,4520],{"class":256},[165,4544,682],{"class":194},[165,4546,1297],{"class":194},[165,4548,4549,4552,4554,4556,4558,4560],{"class":167,"line":348},[165,4550,4551],{"class":307},"      email",[165,4553,451],{"class":194},[165,4555,4496],{"class":256},[165,4557,46],{"class":194},[165,4559,4520],{"class":256},[165,4561,384],{"class":194},[165,4563,4564,4567,4569,4571,4573,4576],{"class":167,"line":353},[165,4565,4566],{"class":307},"      name",[165,4568,451],{"class":194},[165,4570,4496],{"class":256},[165,4572,46],{"class":194},[165,4574,4575],{"class":256},"name",[165,4577,384],{"class":194},[165,4579,4580,4583,4585,4587,4589,4592],{"class":167,"line":373},[165,4581,4582],{"class":307},"      phone",[165,4584,451],{"class":194},[165,4586,4496],{"class":256},[165,4588,46],{"class":194},[165,4590,4591],{"class":256},"phone",[165,4593,384],{"class":194},[165,4595,4596,4599,4601,4603,4605,4608],{"class":167,"line":387},[165,4597,4598],{"class":307},"      websiteUrl",[165,4600,451],{"class":194},[165,4602,4496],{"class":256},[165,4604,46],{"class":194},[165,4606,4607],{"class":256},"websiteUrl",[165,4609,384],{"class":194},[165,4611,4612,4615,4617],{"class":167,"line":401},[165,4613,4614],{"class":194},"    }",[165,4616,343],{"class":307},[165,4618,195],{"class":194},[165,4620,4621],{"class":167,"line":413},[165,4622,725],{"class":194},[165,4624,4625,4627,4629],{"class":167,"line":421},[165,4626,329],{"class":194},[165,4628,343],{"class":256},[165,4630,195],{"class":194},[15,4632,4633],{},"Used email as a unique key to deduplicate agency information across properties, ensuring data consistency while minimizing memory usage.",[148,4635,4637],{"id":4636},"edge-cases-handled","Edge Cases Handled",[1070,4639,4640,4646,4652,4658,4664],{},[105,4641,4642,4645],{},[794,4643,4644],{},"Partial File Completion",": The system tracks progress and can resume from the last successful batch",[105,4647,4648,4651],{},[794,4649,4650],{},"API Token Management",": Graceful handling of token expiration with clear error messages",[105,4653,4654,4657],{},[794,4655,4656],{},"Data Quality",": Handling of missing or malformed data without breaking the pipeline",[105,4659,4660,4663],{},[794,4661,4662],{},"Process Interruption",": File-based checkpointing enables resume-ability",[105,4665,4666,4669],{},[794,4667,4668],{},"Memory Management",": Streaming approach to handle large datasets efficiently",[19,4671,1048],{"id":1047},[102,4673,4674],{},[105,4675,4676],{},[794,4677,4678],{},"Batch Processing Trade-offs",[15,4680,4681],{},"Initially, I tried processing all data in memory. This worked fine during testing with small datasets but quickly became problematic when dealing with the full dataset. Breaking the data into chunks with file-based checkpointing proved more reliable, though slightly slower.",[15,4683,4684],{},"The key insight? Sometimes trading raw speed for reliability is the right choice, especially when dealing with large-scale data extraction.",[102,4686,4687],{"start":49},[105,4688,4689],{},[794,4690,4691],{},"Rate Limiting Strategy",[156,4693,4695],{"className":158,"code":4694,"language":160,"meta":48,"style":48},"await delay(Math.random() * 1000); \u002F\u002F Random delay between 0-1 seconds\n",[162,4696,4697],{"__ignoreMap":48},[165,4698,4699,4701,4703,4706,4708,4711,4714,4717,4719,4721,4723],{"class":167,"line":168},[165,4700,807],{"class":252},[165,4702,4378],{"class":303},[165,4704,4705],{"class":256},"(Math",[165,4707,46],{"class":194},[165,4709,4710],{"class":303},"random",[165,4712,4713],{"class":256},"() ",[165,4715,4716],{"class":179},"*",[165,4718,773],{"class":226},[165,4720,343],{"class":256},[165,4722,230],{"class":194},[165,4724,4725],{"class":233}," \u002F\u002F Random delay between 0-1 seconds\n",[15,4727,4728],{},"Instead of fixed delays, implementing random delays between requests helped avoid predictable patterns that might trigger API defenses. This simple change made our script behave more like natural traffic.",[102,4730,4731],{"start":216},[105,4732,4733],{},[794,4734,4735],{},"Data Integrity > Speed",[15,4737,4738],{},"Using a Map for deduplication was more efficient than array-based filtering, especially when dealing with tens of thousands of records. The slight memory overhead was worth it for the guaranteed uniqueness and O(1) lookup times.",[102,4740,4741],{"start":237},[105,4742,4743],{},[794,4744,4745],{},"Progress Visibility",[156,4747,4749],{"className":158,"code":4748,"language":160,"meta":48,"style":48},"console.log(\n  `Saved ${skip + allProperties.length}\u002F${total} properties (${(\n    ((skip + allProperties.length) \u002F total) *\n    100\n  ).toFixed(2)}%)`,\n);\n",[162,4750,4751,4761,4800,4827,4832,4856],{"__ignoreMap":48},[165,4752,4753,4755,4757,4759],{"class":167,"line":168},[165,4754,852],{"class":256},[165,4756,46],{"class":194},[165,4758,304],{"class":303},[165,4760,370],{"class":256},[165,4762,4763,4765,4768,4770,4773,4775,4777,4779,4782,4784,4786,4788,4791,4793,4796,4798],{"class":167,"line":49},[165,4764,1421],{"class":183},[165,4766,4767],{"class":187},"Saved ",[165,4769,317],{"class":183},[165,4771,4772],{"class":256},"skip",[165,4774,323],{"class":179},[165,4776,4164],{"class":256},[165,4778,46],{"class":183},[165,4780,4781],{"class":175},"length",[165,4783,329],{"class":183},[165,4785,931],{"class":187},[165,4787,317],{"class":183},[165,4789,4790],{"class":256},"total",[165,4792,329],{"class":183},[165,4794,4795],{"class":187}," properties (",[165,4797,317],{"class":183},[165,4799,370],{"class":1659},[165,4801,4802,4805,4807,4809,4811,4813,4815,4817,4819,4822,4824],{"class":167,"line":216},[165,4803,4804],{"class":1659},"    ((",[165,4806,4772],{"class":256},[165,4808,323],{"class":179},[165,4810,4164],{"class":256},[165,4812,46],{"class":183},[165,4814,4781],{"class":175},[165,4816,289],{"class":1659},[165,4818,931],{"class":179},[165,4820,4821],{"class":256}," total",[165,4823,289],{"class":1659},[165,4825,4826],{"class":179},"*\n",[165,4828,4829],{"class":167,"line":237},[165,4830,4831],{"class":226},"    100\n",[165,4833,4834,4836,4838,4841,4843,4845,4847,4849,4852,4854],{"class":167,"line":243},[165,4835,416],{"class":1659},[165,4837,46],{"class":183},[165,4839,4840],{"class":303},"toFixed",[165,4842,308],{"class":1659},[165,4844,1685],{"class":226},[165,4846,343],{"class":1659},[165,4848,329],{"class":183},[165,4850,4851],{"class":187},"%)",[165,4853,311],{"class":183},[165,4855,384],{"class":194},[165,4857,4858,4860],{"class":167,"line":249},[165,4859,343],{"class":256},[165,4861,195],{"class":194},[15,4863,4864],{},"Adding detailed progress logging helped track long-running processes and identify bottlenecks. This became invaluable when the client asked for status updates or when debugging issues.",[19,4866,1119],{"id":1118},[102,4868,4869,4884,4900,4916],{},[105,4870,4871,4873],{},[794,4872,2392],{},[1070,4874,4875,4878,4881],{},[105,4876,4877],{},"Implement parallel processing with worker threads",[105,4879,4880],{},"Investigate streaming CSV generation for lower memory usage",[105,4882,4883],{},"Add batch size auto-tuning based on API response times",[105,4885,4886,4889],{},[794,4887,4888],{},"Data Validation",[1070,4890,4891,4894,4897],{},[105,4892,4893],{},"Add JSON schema validation for API responses",[105,4895,4896],{},"Implement data quality scoring",[105,4898,4899],{},"Add automated anomaly detection",[105,4901,4902,4905],{},[794,4903,4904],{},"Monitoring & Observability",[1070,4906,4907,4910,4913],{},[105,4908,4909],{},"Add proper metrics collection",[105,4911,4912],{},"Implement real-time progress tracking",[105,4914,4915],{},"Create a dashboard for pipeline status",[105,4917,4918,4921],{},[794,4919,4920],{},"Configuration Management",[1070,4922,4923,4926,4929],{},[105,4924,4925],{},"Move hardcoded values to configuration files",[105,4927,4928],{},"Add environment-specific settings",[105,4930,4931],{},"Implement feature flags for different processing modes",[19,4933,1129],{"id":1128},[15,4935,4936],{},"In this case I learned importance of building data pipelines that are not just functional, but also resilient and maintainable. The extra time spent on error handling and progress tracking paid off many times over during the actual data collection phase.",[15,4938,4939],{},"It definitely highlighted how technical challenges often require balancing competing concerns – in this case, speed vs. reliability, and thoroughness vs. API courtesy.",[15,4941,4942],{},"The end result was a robust system that successfully processed nearly 700,000 property records, extracting valuable insights for our client while maintaining data integrity and system reliability throughout the process.",[1154,4944],{},[15,4946,4947],{},[83,4948,4949],{},"Note: This case study has been anonymized to protect client confidentiality while preserving the technical insights and learning opportunities from the project.",[1162,4951,4952],{},"html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sfo-9, html code.shiki .sfo-9{--shiki-light:#90A4AE;--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":48,"searchDepth":49,"depth":49,"links":4954},[4955,4956,4957,4962,4963,4964],{"id":77,"depth":49,"text":78},{"id":125,"depth":49,"text":126},{"id":1239,"depth":49,"text":1240,"children":4958},[4959,4960,4961],{"id":1243,"depth":216,"text":1244},{"id":1272,"depth":216,"text":1273},{"id":4636,"depth":216,"text":4637},{"id":1047,"depth":49,"text":1048},{"id":1118,"depth":49,"text":1119},{"id":1128,"depth":49,"text":1129},"2025-02-26","Developed a robust ETL pipeline to extract, transform, and analyze real estate agency data from a proprietary MLS system, handling rate limits and large datasets efficiently.",{},"\u002Fwork\u002Fproject-dataharvester-etl-pipeline",{"title":3951,"description":4966},"work\u002Fproject-dataharvester-etl-pipeline",[64,3943,3944,4972,4973],"Data Processing","REST API","tbmSJYNZnNxlfYQ8uu37AUNO-vY6-1aVEt6kd8Pawgk",{"id":4976,"title":4977,"body":4978,"date":6146,"description":6147,"extension":55,"externalUrl":1180,"featured":1181,"kind":1182,"meta":6148,"navigation":56,"path":6150,"seo":6151,"stem":6152,"tags":6153,"__hash__":6155},"work\u002Fwork\u002Fpdf-to-platform-automation.md","PDF to digital: automated compliance forms",{"type":8,"value":4979,"toc":6135},[4980,4984,4986,4989,4996,4998,5001,5015,5018,5020,5022,5044,5046,5053,5056,5287,5290,5301,5308,5626,5629,5643,5650,5955,5958,5972,5974,5981,5992,6003,6006,6017,6024,6027,6041,6048,6051,6065,6067,6117,6119,6122,6125,6127,6132],[11,4981,4983],{"id":4982},"automating-pdf-form-digitization-with-llms","Automating PDF form digitization with LLMs",[19,4985,78],{"id":77},[15,4987,4988],{},"While working on partnership development for our compliance platform, we identified an opportunity to demonstrate value to a potential enterprise partner. They had comprehensive compliance templates (QM10 and QM20) available only as PDFs. Instead of traditional sales outreach, I proposed building their templates directly on our platform as a proof of concept.",[87,4990,4991],{},[15,4992,4993],{},[83,4994,4995],{},"Note: While the specific partner and compliance details are kept confidential, this case study focuses on the technical approach to solving a common enterprise challenge: converting unstructured PDF forms into interactive digital workflows.",[19,4997,126],{"id":125},[15,4999,5000],{},"I developed an automated pipeline that could:",[102,5002,5003,5006,5009,5012],{},[105,5004,5005],{},"Extract structured data from complex PDF compliance forms",[105,5007,5008],{},"Transform the data into a standardized format",[105,5010,5011],{},"Automatically generate digital workflows through our platform's GraphQL API",[105,5013,5014],{},"Create a complete, interactive compliance assessment ready for immediate use",[15,5016,5017],{},"The end result was a fully-functional digital version of their compliance workflow that we could demonstrate during partnership discussions.",[19,5019,1240],{"id":1239},[148,5021,1244],{"id":1243},[1070,5023,5024,5028,5034,5038],{},[105,5025,5026,1252],{},[794,5027,1251],{},[105,5029,5030,5033],{},[794,5031,5032],{},"OpenAI GPT API",": PDF content extraction and structuring",[105,5035,5036,1257],{},[794,5037,1190],{},[105,5039,5040,5043],{},[794,5041,5042],{},"PDF Processing",": Initial exploration with PDF parsing libraries",[148,5045,1273],{"id":1272},[102,5047,5048],{},[105,5049,5050],{},[794,5051,5052],{},"LLM-Based Data Extraction",[15,5054,5055],{},"After initially exploring traditional PDF parsing libraries, I pivoted to using OpenAI's GPT for data extraction. Here's why:",[156,5057,5059],{"className":158,"code":5058,"language":160,"meta":48,"style":48},"\u002F\u002F Example of the structured data format we achieved\ninterface ComplianceSection {\n  title: string;\n  informationText: string;\n  goal: string;\n  booleanQuestions: string[];\n  mappingIndication: string[];\n}\n\n\u002F\u002F Sample of extracted and structured data\nconst extractedData = [\n  {\n    title: \"1.2 Informatiebeveiligingsbeleid en bestuurlijke goedkeuring\",\n    informationText: \"Het management van de organisatie dient...\",\n    goal: \"Voorkomen dat er informatiebeveiligingsincidenten...\",\n    booleanQuestions: [\n      \"Heeft de organisatie een gedetailleerd informatiebeveiligingsbeleid...?\",\n      \u002F\u002F More questions...\n    ],\n    mappingIndication: [\n      \"ISO 27001: A.5.1 – Information security policies\",\n      \u002F\u002F More mappings...\n    ],\n  },\n  \u002F\u002F More sections...\n];\n",[162,5060,5061,5066,5075,5086,5097,5108,5121,5134,5138,5142,5147,5158,5162,5178,5194,5210,5219,5230,5235,5241,5250,5261,5266,5272,5276,5281],{"__ignoreMap":48},[165,5062,5063],{"class":167,"line":168},[165,5064,5065],{"class":233},"\u002F\u002F Example of the structured data format we achieved\n",[165,5067,5068,5070,5073],{"class":167,"line":49},[165,5069,1290],{"class":171},[165,5071,5072],{"class":1293}," ComplianceSection",[165,5074,1297],{"class":194},[165,5076,5077,5080,5082,5084],{"class":167,"line":216},[165,5078,5079],{"class":1321},"  title",[165,5081,451],{"class":179},[165,5083,1310],{"class":748},[165,5085,195],{"class":194},[165,5087,5088,5091,5093,5095],{"class":167,"line":237},[165,5089,5090],{"class":1321},"  informationText",[165,5092,451],{"class":179},[165,5094,1310],{"class":748},[165,5096,195],{"class":194},[165,5098,5099,5102,5104,5106],{"class":167,"line":243},[165,5100,5101],{"class":1321},"  goal",[165,5103,451],{"class":179},[165,5105,1310],{"class":748},[165,5107,195],{"class":194},[165,5109,5110,5113,5115,5117,5119],{"class":167,"line":249},[165,5111,5112],{"class":1321},"  booleanQuestions",[165,5114,451],{"class":179},[165,5116,1310],{"class":748},[165,5118,2584],{"class":256},[165,5120,195],{"class":194},[165,5122,5123,5126,5128,5130,5132],{"class":167,"line":295},[165,5124,5125],{"class":1321},"  mappingIndication",[165,5127,451],{"class":179},[165,5129,1310],{"class":748},[165,5131,2584],{"class":256},[165,5133,195],{"class":194},[165,5135,5136],{"class":167,"line":348},[165,5137,784],{"class":194},[165,5139,5140],{"class":167,"line":353},[165,5141,240],{"emptyLinePlaceholder":56},[165,5143,5144],{"class":167,"line":373},[165,5145,5146],{"class":233},"\u002F\u002F Sample of extracted and structured data\n",[165,5148,5149,5151,5154,5156],{"class":167,"line":387},[165,5150,172],{"class":171},[165,5152,5153],{"class":175}," extractedData",[165,5155,180],{"class":179},[165,5157,1899],{"class":256},[165,5159,5160],{"class":167,"line":401},[165,5161,2854],{"class":194},[165,5163,5164,5167,5169,5171,5174,5176],{"class":167,"line":413},[165,5165,5166],{"class":307},"    title",[165,5168,451],{"class":194},[165,5170,184],{"class":183},[165,5172,5173],{"class":187},"1.2 Informatiebeveiligingsbeleid en bestuurlijke goedkeuring",[165,5175,191],{"class":183},[165,5177,384],{"class":194},[165,5179,5180,5183,5185,5187,5190,5192],{"class":167,"line":421},[165,5181,5182],{"class":307},"    informationText",[165,5184,451],{"class":194},[165,5186,184],{"class":183},[165,5188,5189],{"class":187},"Het management van de organisatie dient...",[165,5191,191],{"class":183},[165,5193,384],{"class":194},[165,5195,5196,5199,5201,5203,5206,5208],{"class":167,"line":426},[165,5197,5198],{"class":307},"    goal",[165,5200,451],{"class":194},[165,5202,184],{"class":183},[165,5204,5205],{"class":187},"Voorkomen dat er informatiebeveiligingsincidenten...",[165,5207,191],{"class":183},[165,5209,384],{"class":194},[165,5211,5212,5215,5217],{"class":167,"line":445},[165,5213,5214],{"class":307},"    booleanQuestions",[165,5216,451],{"class":194},[165,5218,1899],{"class":256},[165,5220,5221,5223,5226,5228],{"class":167,"line":472},[165,5222,2913],{"class":183},[165,5224,5225],{"class":187},"Heeft de organisatie een gedetailleerd informatiebeveiligingsbeleid...?",[165,5227,191],{"class":183},[165,5229,384],{"class":194},[165,5231,5232],{"class":167,"line":489},[165,5233,5234],{"class":233},"      \u002F\u002F More questions...\n",[165,5236,5237,5239],{"class":167,"line":499},[165,5238,1994],{"class":256},[165,5240,384],{"class":194},[165,5242,5243,5246,5248],{"class":167,"line":504},[165,5244,5245],{"class":307},"    mappingIndication",[165,5247,451],{"class":194},[165,5249,1899],{"class":256},[165,5251,5252,5254,5257,5259],{"class":167,"line":510},[165,5253,2913],{"class":183},[165,5255,5256],{"class":187},"ISO 27001: A.5.1 – Information security policies",[165,5258,191],{"class":183},[165,5260,384],{"class":194},[165,5262,5263],{"class":167,"line":545},[165,5264,5265],{"class":233},"      \u002F\u002F More mappings...\n",[165,5267,5268,5270],{"class":167,"line":574},[165,5269,1994],{"class":256},[165,5271,384],{"class":194},[165,5273,5274],{"class":167,"line":581},[165,5275,1816],{"class":194},[165,5277,5278],{"class":167,"line":586},[165,5279,5280],{"class":233},"  \u002F\u002F More sections...\n",[165,5282,5283,5285],{"class":167,"line":601},[165,5284,942],{"class":256},[165,5286,195],{"class":194},[15,5288,5289],{},"This approach provided superior results compared to traditional PDF parsing because:",[1070,5291,5292,5295,5298],{},[105,5293,5294],{},"PDFs contained complex formatting and tables",[105,5296,5297],{},"LLM could understand context and relationships between elements",[105,5299,5300],{},"Structured output was more reliable and required less cleanup",[102,5302,5303],{"start":49},[105,5304,5305],{},[794,5306,5307],{},"Automated Form Generation Pipeline",[156,5309,5311],{"className":158,"code":5310,"language":160,"meta":48,"style":48},"async function main() {\n  \u002F\u002F Authentication\n  await signIn(baseUrl, process.env.USERNAME, process.env.PASSWORD);\n\n  \u002F\u002F Create base form structure\n  const formCollection = await createFormCollection({\n    tenantId: tenant.id,\n    data: { name: \"QM20 Assessment Form\" },\n  });\n\n  \u002F\u002F Process each section from extracted data\n  for (const section of extractedData) {\n    const formSection = await createFormSection({\n      tenantId: tenant.id,\n      formId: formId,\n      data: {\n        title: section.title,\n        description: section.informationText,\n      },\n    });\n\n    \u002F\u002F Create dynamic form fields\n    await createFormFields(tenant.id, formSection.id, section);\n  }\n}\n",[162,5312,5313,5326,5331,5375,5379,5384,5402,5416,5439,5447,5451,5456,5477,5495,5510,5522,5531,5547,5563,5568,5576,5580,5585,5618,5622],{"__ignoreMap":48},[165,5314,5315,5317,5319,5322,5324],{"class":167,"line":168},[165,5316,3380],{"class":171},[165,5318,3383],{"class":171},[165,5320,5321],{"class":303}," main",[165,5323,914],{"class":194},[165,5325,1297],{"class":194},[165,5327,5328],{"class":167,"line":49},[165,5329,5330],{"class":233},"  \u002F\u002F Authentication\n",[165,5332,5333,5335,5338,5340,5343,5345,5348,5350,5353,5355,5358,5360,5362,5364,5366,5368,5371,5373],{"class":167,"line":216},[165,5334,742],{"class":252},[165,5336,5337],{"class":303}," signIn",[165,5339,308],{"class":307},[165,5341,5342],{"class":256},"baseUrl",[165,5344,682],{"class":194},[165,5346,5347],{"class":256}," process",[165,5349,46],{"class":194},[165,5351,5352],{"class":256},"env",[165,5354,46],{"class":194},[165,5356,5357],{"class":175},"USERNAME",[165,5359,682],{"class":194},[165,5361,5347],{"class":256},[165,5363,46],{"class":194},[165,5365,5352],{"class":256},[165,5367,46],{"class":194},[165,5369,5370],{"class":175},"PASSWORD",[165,5372,343],{"class":307},[165,5374,195],{"class":194},[165,5376,5377],{"class":167,"line":237},[165,5378,240],{"emptyLinePlaceholder":56},[165,5380,5381],{"class":167,"line":243},[165,5382,5383],{"class":233},"  \u002F\u002F Create base form structure\n",[165,5385,5386,5388,5391,5393,5395,5398,5400],{"class":167,"line":249},[165,5387,356],{"class":171},[165,5389,5390],{"class":175}," formCollection",[165,5392,180],{"class":179},[165,5394,364],{"class":252},[165,5396,5397],{"class":303}," createFormCollection",[165,5399,308],{"class":307},[165,5401,292],{"class":194},[165,5403,5404,5406,5408,5410,5412,5414],{"class":167,"line":295},[165,5405,475],{"class":307},[165,5407,451],{"class":194},[165,5409,480],{"class":256},[165,5411,46],{"class":194},[165,5413,381],{"class":256},[165,5415,384],{"class":194},[165,5417,5418,5421,5423,5425,5428,5430,5432,5435,5437],{"class":167,"line":348},[165,5419,5420],{"class":307},"    data",[165,5422,451],{"class":194},[165,5424,4493],{"class":194},[165,5426,5427],{"class":307}," name",[165,5429,451],{"class":194},[165,5431,184],{"class":183},[165,5433,5434],{"class":187},"QM20 Assessment Form",[165,5436,191],{"class":183},[165,5438,1931],{"class":194},[165,5440,5441,5443,5445],{"class":167,"line":353},[165,5442,492],{"class":194},[165,5444,343],{"class":307},[165,5446,195],{"class":194},[165,5448,5449],{"class":167,"line":373},[165,5450,240],{"emptyLinePlaceholder":56},[165,5452,5453],{"class":167,"line":387},[165,5454,5455],{"class":233},"  \u002F\u002F Process each section from extracted data\n",[165,5457,5458,5461,5463,5465,5468,5471,5473,5475],{"class":167,"line":401},[165,5459,5460],{"class":252},"  for",[165,5462,257],{"class":307},[165,5464,172],{"class":171},[165,5466,5467],{"class":175}," section",[165,5469,5470],{"class":179}," of",[165,5472,5153],{"class":256},[165,5474,289],{"class":307},[165,5476,292],{"class":194},[165,5478,5479,5481,5484,5486,5488,5491,5493],{"class":167,"line":413},[165,5480,604],{"class":171},[165,5482,5483],{"class":175}," formSection",[165,5485,180],{"class":179},[165,5487,364],{"class":252},[165,5489,5490],{"class":303}," createFormSection",[165,5492,308],{"class":307},[165,5494,292],{"class":194},[165,5496,5497,5500,5502,5504,5506,5508],{"class":167,"line":421},[165,5498,5499],{"class":307},"      tenantId",[165,5501,451],{"class":194},[165,5503,480],{"class":256},[165,5505,46],{"class":194},[165,5507,381],{"class":256},[165,5509,384],{"class":194},[165,5511,5512,5515,5517,5520],{"class":167,"line":426},[165,5513,5514],{"class":307},"      formId",[165,5516,451],{"class":194},[165,5518,5519],{"class":256}," formId",[165,5521,384],{"class":194},[165,5523,5524,5527,5529],{"class":167,"line":445},[165,5525,5526],{"class":307},"      data",[165,5528,451],{"class":194},[165,5530,1297],{"class":194},[165,5532,5533,5536,5538,5540,5542,5545],{"class":167,"line":472},[165,5534,5535],{"class":307},"        title",[165,5537,451],{"class":194},[165,5539,5467],{"class":256},[165,5541,46],{"class":194},[165,5543,5544],{"class":256},"title",[165,5546,384],{"class":194},[165,5548,5549,5552,5554,5556,5558,5561],{"class":167,"line":489},[165,5550,5551],{"class":307},"        description",[165,5553,451],{"class":194},[165,5555,5467],{"class":256},[165,5557,46],{"class":194},[165,5559,5560],{"class":256},"informationText",[165,5562,384],{"class":194},[165,5564,5565],{"class":167,"line":499},[165,5566,5567],{"class":194},"      },\n",[165,5569,5570,5572,5574],{"class":167,"line":504},[165,5571,4614],{"class":194},[165,5573,343],{"class":307},[165,5575,195],{"class":194},[165,5577,5578],{"class":167,"line":510},[165,5579,240],{"emptyLinePlaceholder":56},[165,5581,5582],{"class":167,"line":545},[165,5583,5584],{"class":233},"    \u002F\u002F Create dynamic form fields\n",[165,5586,5587,5590,5593,5595,5598,5600,5602,5604,5606,5608,5610,5612,5614,5616],{"class":167,"line":574},[165,5588,5589],{"class":252},"    await",[165,5591,5592],{"class":303}," createFormFields",[165,5594,308],{"class":307},[165,5596,5597],{"class":256},"tenant",[165,5599,46],{"class":194},[165,5601,381],{"class":256},[165,5603,682],{"class":194},[165,5605,5483],{"class":256},[165,5607,46],{"class":194},[165,5609,381],{"class":256},[165,5611,682],{"class":194},[165,5613,5467],{"class":256},[165,5615,343],{"class":307},[165,5617,195],{"class":194},[165,5619,5620],{"class":167,"line":581},[165,5621,725],{"class":194},[165,5623,5624],{"class":167,"line":586},[165,5625,784],{"class":194},[15,5627,5628],{},"The pipeline handles:",[1070,5630,5631,5634,5637,5640],{},[105,5632,5633],{},"Authentication and session management",[105,5635,5636],{},"Hierarchical form creation (collections → sections → fields)",[105,5638,5639],{},"Dynamic field generation based on question types",[105,5641,5642],{},"Metadata and mapping preservation",[102,5644,5645],{"start":216},[105,5646,5647],{},[794,5648,5649],{},"Error Handling and Validation",[156,5651,5653],{"className":158,"code":5652,"language":160,"meta":48,"style":48},"const createFormField = async ({\n  tenantId,\n  formSectionId,\n  type,\n  initialTitle,\n}) => {\n  try {\n    const field = await createField(\u002F* ... *\u002F);\n    await updateFormField(tenant.id, field.id, {\n      richDescription: \"\",\n      richTitle: initialTitle,\n      disabled: false,\n      metadata: {\n        validation: {\n          required: false,\n          formats: [\"PDF\", \"DOC\", \"DOCX\" \u002F* ... *\u002F],\n        },\n      },\n    });\n    return field;\n  } catch (error) {\n    console.error(`Failed to create field: ${initialTitle}`);\n    throw error;\n  }\n};\n",[162,5654,5655,5672,5678,5685,5691,5698,5709,5716,5738,5764,5776,5788,5801,5810,5819,5830,5872,5877,5881,5889,5897,5911,5937,5947,5951],{"__ignoreMap":48},[165,5656,5657,5659,5663,5665,5668,5670],{"class":167,"line":168},[165,5658,172],{"class":171},[165,5660,5662],{"class":5661},"sfCm-"," createFormField",[165,5664,180],{"class":179},[165,5666,5667],{"class":171}," async",[165,5669,257],{"class":256},[165,5671,292],{"class":194},[165,5673,5674,5676],{"class":167,"line":49},[165,5675,1403],{"class":256},[165,5677,384],{"class":194},[165,5679,5680,5683],{"class":167,"line":216},[165,5681,5682],{"class":256},"  formSectionId",[165,5684,384],{"class":194},[165,5686,5687,5689],{"class":167,"line":237},[165,5688,2067],{"class":256},[165,5690,384],{"class":194},[165,5692,5693,5696],{"class":167,"line":243},[165,5694,5695],{"class":256},"  initialTitle",[165,5697,384],{"class":194},[165,5699,5700,5702,5704,5707],{"class":167,"line":249},[165,5701,329],{"class":194},[165,5703,289],{"class":256},[165,5705,5706],{"class":171},"=>",[165,5708,1297],{"class":194},[165,5710,5711,5714],{"class":167,"line":295},[165,5712,5713],{"class":252},"  try",[165,5715,1297],{"class":194},[165,5717,5718,5720,5722,5724,5726,5729,5731,5734,5736],{"class":167,"line":348},[165,5719,604],{"class":171},[165,5721,1844],{"class":175},[165,5723,180],{"class":179},[165,5725,364],{"class":252},[165,5727,5728],{"class":303}," createField",[165,5730,308],{"class":307},[165,5732,5733],{"class":233},"\u002F* ... *\u002F",[165,5735,343],{"class":307},[165,5737,195],{"class":194},[165,5739,5740,5742,5744,5746,5748,5750,5752,5754,5756,5758,5760,5762],{"class":167,"line":353},[165,5741,5589],{"class":252},[165,5743,1836],{"class":303},[165,5745,308],{"class":307},[165,5747,5597],{"class":256},[165,5749,46],{"class":194},[165,5751,381],{"class":256},[165,5753,682],{"class":194},[165,5755,1844],{"class":256},[165,5757,46],{"class":194},[165,5759,381],{"class":256},[165,5761,682],{"class":194},[165,5763,1297],{"class":194},[165,5765,5766,5769,5771,5774],{"class":167,"line":373},[165,5767,5768],{"class":307},"      richDescription",[165,5770,451],{"class":194},[165,5772,5773],{"class":183}," \"\"",[165,5775,384],{"class":194},[165,5777,5778,5781,5783,5786],{"class":167,"line":387},[165,5779,5780],{"class":307},"      richTitle",[165,5782,451],{"class":194},[165,5784,5785],{"class":256}," initialTitle",[165,5787,384],{"class":194},[165,5789,5790,5793,5795,5799],{"class":167,"line":401},[165,5791,5792],{"class":307},"      disabled",[165,5794,451],{"class":194},[165,5796,5798],{"class":5797},"syTEX"," false",[165,5800,384],{"class":194},[165,5802,5803,5806,5808],{"class":167,"line":413},[165,5804,5805],{"class":307},"      metadata",[165,5807,451],{"class":194},[165,5809,1297],{"class":194},[165,5811,5812,5815,5817],{"class":167,"line":421},[165,5813,5814],{"class":307},"        validation",[165,5816,451],{"class":194},[165,5818,1297],{"class":194},[165,5820,5821,5824,5826,5828],{"class":167,"line":426},[165,5822,5823],{"class":307},"          required",[165,5825,451],{"class":194},[165,5827,5798],{"class":5797},[165,5829,384],{"class":194},[165,5831,5832,5835,5837,5840,5842,5845,5847,5849,5851,5854,5856,5858,5860,5863,5865,5868,5870],{"class":167,"line":445},[165,5833,5834],{"class":307},"          formats",[165,5836,451],{"class":194},[165,5838,5839],{"class":307}," [",[165,5841,191],{"class":183},[165,5843,5844],{"class":187},"PDF",[165,5846,191],{"class":183},[165,5848,682],{"class":194},[165,5850,184],{"class":183},[165,5852,5853],{"class":187},"DOC",[165,5855,191],{"class":183},[165,5857,682],{"class":194},[165,5859,184],{"class":183},[165,5861,5862],{"class":187},"DOCX",[165,5864,191],{"class":183},[165,5866,5867],{"class":233}," \u002F* ... *\u002F",[165,5869,942],{"class":307},[165,5871,384],{"class":194},[165,5873,5874],{"class":167,"line":472},[165,5875,5876],{"class":194},"        },\n",[165,5878,5879],{"class":167,"line":489},[165,5880,5567],{"class":194},[165,5882,5883,5885,5887],{"class":167,"line":499},[165,5884,4614],{"class":194},[165,5886,343],{"class":307},[165,5888,195],{"class":194},[165,5890,5891,5893,5895],{"class":167,"line":504},[165,5892,4360],{"class":252},[165,5894,1844],{"class":256},[165,5896,195],{"class":194},[165,5898,5899,5901,5903,5905,5907,5909],{"class":167,"line":510},[165,5900,492],{"class":194},[165,5902,3538],{"class":252},[165,5904,257],{"class":307},[165,5906,3574],{"class":256},[165,5908,289],{"class":307},[165,5910,292],{"class":194},[165,5912,5913,5915,5917,5919,5921,5923,5926,5928,5931,5933,5935],{"class":167,"line":545},[165,5914,4332],{"class":256},[165,5916,46],{"class":194},[165,5918,3574],{"class":303},[165,5920,308],{"class":307},[165,5922,311],{"class":183},[165,5924,5925],{"class":187},"Failed to create field: ",[165,5927,317],{"class":183},[165,5929,5930],{"class":256},"initialTitle",[165,5932,340],{"class":183},[165,5934,343],{"class":307},[165,5936,195],{"class":194},[165,5938,5939,5942,5945],{"class":167,"line":574},[165,5940,5941],{"class":252},"    throw",[165,5943,5944],{"class":256}," error",[165,5946,195],{"class":194},[165,5948,5949],{"class":167,"line":581},[165,5950,725],{"class":194},[165,5952,5953],{"class":167,"line":586},[165,5954,1515],{"class":194},[15,5956,5957],{},"Built-in safeguards include:",[1070,5959,5960,5963,5966,5969],{},[105,5961,5962],{},"Proper error handling for API calls",[105,5964,5965],{},"Field validation rules",[105,5967,5968],{},"File format restrictions",[105,5970,5971],{},"Rich text support for complex content",[19,5973,1048],{"id":1047},[102,5975,5976],{},[105,5977,5978],{},[794,5979,5980],{},"PDF Data Extraction Strategy",[15,5982,5983,5984,5987,5988,5991],{},"The initial approach using PDF parsing libraries like ",[162,5985,5986],{},"pdf-parse"," or ",[162,5989,5990],{},"pdf2json"," proved challenging due to:",[1070,5993,5994,5997,6000],{},[105,5995,5996],{},"Inconsistent text extraction",[105,5998,5999],{},"Loss of formatting and structure",[105,6001,6002],{},"Difficulty handling tables and layouts",[15,6004,6005],{},"LLMs provided a more elegant solution by:",[1070,6007,6008,6011,6014],{},[105,6009,6010],{},"Understanding document context",[105,6012,6013],{},"Maintaining relationships between elements",[105,6015,6016],{},"Producing clean, structured output",[102,6018,6019],{"start":49},[105,6020,6021],{},[794,6022,6023],{},"GraphQL API Orchestration",[15,6025,6026],{},"Managing multiple dependent API calls required careful orchestration:",[1070,6028,6029,6032,6035,6038],{},[105,6030,6031],{},"Sequential processing for proper parent-child relationships",[105,6033,6034],{},"Error handling with appropriate rollbacks",[105,6036,6037],{},"Rate limiting consideration",[105,6039,6040],{},"Progress tracking for long-running operations",[102,6042,6043],{"start":216},[105,6044,6045],{},[794,6046,6047],{},"Business Process Automation",[15,6049,6050],{},"The project highlighted how technical solutions can directly impact business development:",[1070,6052,6053,6056,6059,6062],{},[105,6054,6055],{},"Reduced sales cycle by providing immediate value",[105,6057,6058],{},"Demonstrated platform capabilities effectively",[105,6060,6061],{},"Saved significant manual work for the CSM team",[105,6063,6064],{},"Created reusable automation patterns",[19,6066,1119],{"id":1118},[102,6068,6069,6085,6101],{},[105,6070,6071,6074],{},[794,6072,6073],{},"Scalability Improvements",[1070,6075,6076,6079,6082],{},[105,6077,6078],{},"Batch processing for multiple PDFs",[105,6080,6081],{},"Parallel processing where possible",[105,6083,6084],{},"Caching for improved performance",[105,6086,6087,6090],{},[794,6088,6089],{},"Enhanced Extraction",[1070,6091,6092,6095,6098],{},[105,6093,6094],{},"Support for more complex PDF layouts",[105,6096,6097],{},"Additional compliance template types",[105,6099,6100],{},"Multi-language support",[105,6102,6103,6106],{},[794,6104,6105],{},"Integration Enhancements",[1070,6107,6108,6111,6114],{},[105,6109,6110],{},"Automated testing for generated forms",[105,6112,6113],{},"Version control for templates",[105,6115,6116],{},"Change tracking and diff generation",[19,6118,1129],{"id":1128},[15,6120,6121],{},"I learned the power of combining modern AI tools with traditional automation to solve real business challenges. By thinking creatively about PDF data extraction and leveraging LLMs, I turned what could have been weeks of manual work into an automated process.",[15,6123,6124],{},"The solution saved immediate time and resources + created a repeatable pattern for future partner onboarding. Most importantly, it transformed a traditional sales approach into a value-first demonstration which resonated with our potential partner.",[1154,6126],{},[15,6128,6129],{},[83,6130,6131],{},"Note: This case study focuses on the technical implementation while respecting confidentiality around specific partner details and compliance requirements.",[1162,6133,6134],{},"html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sucvu, html code.shiki .sucvu{--shiki-light:#E53935;--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sfCm-, html code.shiki .sfCm-{--shiki-light:#90A4AE;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":48,"searchDepth":49,"depth":49,"links":6136},[6137,6138,6139,6143,6144,6145],{"id":77,"depth":49,"text":78},{"id":125,"depth":49,"text":126},{"id":1239,"depth":49,"text":1240,"children":6140},[6141,6142],{"id":1243,"depth":216,"text":1244},{"id":1272,"depth":216,"text":1273},{"id":1047,"depth":49,"text":1048},{"id":1118,"depth":49,"text":1119},{"id":1128,"depth":49,"text":1129},"2025-01-20","Built an automated pipeline to transform complex PDF compliance forms into digital workflows, leveraging LLMs for structured data extraction and GraphQL for platform integration.",{"slug":6149},"pdf-to-platform-automation","\u002Fwork\u002Fpdf-to-platform-automation",{"title":4977,"description":6147},"work\u002Fpdf-to-platform-automation",[64,6154,3944,1190,1189],"OpenAI","SYRiH8RVDeh3lQyrdBXhGlg40Y-Mx4zEc2Mk9-o-lR8",{"id":6157,"title":6158,"body":6159,"date":7234,"description":7235,"extension":55,"externalUrl":1180,"featured":1181,"kind":1182,"meta":7236,"navigation":56,"path":7238,"seo":7239,"stem":7240,"tags":7241,"__hash__":7245},"work\u002Fwork\u002Fvideo-transcript-analysis-automation.md","LLM-backed customer interview analysis",{"type":8,"value":6160,"toc":7220},[6161,6164,6166,6169,6176,6178,6181,6198,6200,6204,6207,6312,6315,6326,6330,6333,6646,6650,6653,6670,7003,7007,7010,7024,7027,7035,7037,7090,7094,7097,7147,7149,7199,7201,7204,7210,7212,7217],[11,6162,6158],{"id":6163},"llm-backed-customer-interview-analysis",[19,6165,78],{"id":77},[15,6167,6168],{},"A critical customer interview was recorded using an AI-powered meeting platform. The conversation contained invaluable insights about our Ideal Customer Profile (ICP) and user motivations, but it was locked inside a video recording. The marketing team needed these insights in a format they could analyze and use, specifically focusing on a crucial segment from minute 52 onwards.",[87,6170,6171],{},[15,6172,6173],{},[83,6174,6175],{},"Note: While specific platform and customer details are anonymized, this case study demonstrates how technical creativity can unlock marketing value from everyday customer interactions.",[19,6177,126],{"id":125},[15,6179,6180],{},"I developed a pipeline that could:",[102,6182,6183,6186,6189,6192,6195],{},[105,6184,6185],{},"Extract structured transcript data from the platform's network requests",[105,6187,6188],{},"Transform complex nested JSON into readable dialogue",[105,6190,6191],{},"Filter conversations by timestamp ranges",[105,6193,6194],{},"Format the output for both human reading and LLM processing",[105,6196,6197],{},"Generate marketing content while preserving customer privacy",[19,6199,1240],{"id":1239},[148,6201,6203],{"id":6202},"_1-network-request-analysis","1. Network Request Analysis",[15,6205,6206],{},"The platform's UI showed a nicely formatted transcript, suggesting the data was already structured somewhere. Using Chrome DevTools' Network tab, I found a large JSON payload containing the entire conversation structure.",[156,6208,6210],{"className":158,"code":6209,"language":160,"meta":48,"style":48},"interface TranscriptCue {\n  id: number;\n  text: string;\n  speaker_name: string;\n  speaker_email: string;\n  is_host: boolean;\n  started_at: string;\n  start_time: number;\n  end_time: number;\n}\n",[162,6211,6212,6221,6231,6242,6253,6264,6275,6286,6297,6308],{"__ignoreMap":48},[165,6213,6214,6216,6219],{"class":167,"line":168},[165,6215,1290],{"class":171},[165,6217,6218],{"class":1293}," TranscriptCue",[165,6220,1297],{"class":194},[165,6222,6223,6225,6227,6229],{"class":167,"line":49},[165,6224,1484],{"class":1321},[165,6226,451],{"class":179},[165,6228,3409],{"class":748},[165,6230,195],{"class":194},[165,6232,6233,6236,6238,6240],{"class":167,"line":216},[165,6234,6235],{"class":1321},"  text",[165,6237,451],{"class":179},[165,6239,1310],{"class":748},[165,6241,195],{"class":194},[165,6243,6244,6247,6249,6251],{"class":167,"line":237},[165,6245,6246],{"class":1321},"  speaker_name",[165,6248,451],{"class":179},[165,6250,1310],{"class":748},[165,6252,195],{"class":194},[165,6254,6255,6258,6260,6262],{"class":167,"line":243},[165,6256,6257],{"class":1321},"  speaker_email",[165,6259,451],{"class":179},[165,6261,1310],{"class":748},[165,6263,195],{"class":194},[165,6265,6266,6269,6271,6273],{"class":167,"line":249},[165,6267,6268],{"class":1321},"  is_host",[165,6270,451],{"class":179},[165,6272,2645],{"class":748},[165,6274,195],{"class":194},[165,6276,6277,6280,6282,6284],{"class":167,"line":295},[165,6278,6279],{"class":1321},"  started_at",[165,6281,451],{"class":179},[165,6283,1310],{"class":748},[165,6285,195],{"class":194},[165,6287,6288,6291,6293,6295],{"class":167,"line":348},[165,6289,6290],{"class":1321},"  start_time",[165,6292,451],{"class":179},[165,6294,3409],{"class":748},[165,6296,195],{"class":194},[165,6298,6299,6302,6304,6306],{"class":167,"line":353},[165,6300,6301],{"class":1321},"  end_time",[165,6303,451],{"class":179},[165,6305,3409],{"class":748},[165,6307,195],{"class":194},[165,6309,6310],{"class":167,"line":373},[165,6311,784],{"class":194},[15,6313,6314],{},"The challenge was that the data structure was deeply nested and non-intuitive:",[1070,6316,6317,6320,6323],{},[105,6318,6319],{},"Each cue had numbered properties",[105,6321,6322],{},"Timestamps were in seconds with decimals",[105,6324,6325],{},"Speaker information was scattered across different objects",[148,6327,6329],{"id":6328},"_2-timestamp-processing","2. Timestamp Processing",[15,6331,6332],{},"The marketing team wanted a specific segment (52:00 onwards). I built functions to handle timestamp conversion and filtering:",[156,6334,6336],{"className":158,"code":6335,"language":160,"meta":48,"style":48},"function formatTimestamp(seconds: number): string {\n  const minutes = Math.floor(seconds \u002F 60);\n  const remainingSeconds = Math.floor(seconds % 60);\n  return `${minutes}:${remainingSeconds.toString().padStart(2, \"0\")}`;\n}\n\nfunction parseTimestamp(line: string): number {\n  const match = line.match(\u002F\\[(\\d+):(\\d+)\\]\u002F);\n  if (match) {\n    const minutes = parseInt(match[1]);\n    const seconds = parseInt(match[2]);\n    return minutes * 60 + seconds;\n  }\n  return 0;\n}\n",[162,6337,6338,6362,6393,6421,6467,6471,6475,6498,6554,6566,6590,6613,6630,6634,6642],{"__ignoreMap":48},[165,6339,6340,6342,6345,6347,6350,6352,6354,6356,6358,6360],{"class":167,"line":168},[165,6341,2783],{"class":171},[165,6343,6344],{"class":303}," formatTimestamp",[165,6346,308],{"class":194},[165,6348,6349],{"class":536},"seconds",[165,6351,451],{"class":179},[165,6353,3409],{"class":748},[165,6355,343],{"class":194},[165,6357,451],{"class":179},[165,6359,1310],{"class":748},[165,6361,1297],{"class":194},[165,6363,6364,6366,6369,6371,6374,6376,6379,6381,6383,6386,6389,6391],{"class":167,"line":49},[165,6365,356],{"class":171},[165,6367,6368],{"class":175}," minutes",[165,6370,180],{"class":179},[165,6372,6373],{"class":256}," Math",[165,6375,46],{"class":194},[165,6377,6378],{"class":303},"floor",[165,6380,308],{"class":307},[165,6382,6349],{"class":256},[165,6384,6385],{"class":179}," \u002F",[165,6387,6388],{"class":226}," 60",[165,6390,343],{"class":307},[165,6392,195],{"class":194},[165,6394,6395,6397,6400,6402,6404,6406,6408,6410,6412,6415,6417,6419],{"class":167,"line":216},[165,6396,356],{"class":171},[165,6398,6399],{"class":175}," remainingSeconds",[165,6401,180],{"class":179},[165,6403,6373],{"class":256},[165,6405,46],{"class":194},[165,6407,6378],{"class":303},[165,6409,308],{"class":307},[165,6411,6349],{"class":256},[165,6413,6414],{"class":179}," %",[165,6416,6388],{"class":226},[165,6418,343],{"class":307},[165,6420,195],{"class":194},[165,6422,6423,6425,6427,6430,6432,6434,6436,6439,6441,6443,6445,6447,6449,6451,6453,6455,6457,6459,6461,6463,6465],{"class":167,"line":237},[165,6424,3341],{"class":252},[165,6426,671],{"class":183},[165,6428,6429],{"class":256},"minutes",[165,6431,329],{"class":183},[165,6433,451],{"class":187},[165,6435,317],{"class":183},[165,6437,6438],{"class":256},"remainingSeconds",[165,6440,46],{"class":183},[165,6442,1673],{"class":303},[165,6444,914],{"class":1659},[165,6446,46],{"class":183},[165,6448,1680],{"class":303},[165,6450,308],{"class":1659},[165,6452,1685],{"class":226},[165,6454,682],{"class":183},[165,6456,184],{"class":183},[165,6458,1692],{"class":187},[165,6460,191],{"class":183},[165,6462,343],{"class":1659},[165,6464,340],{"class":183},[165,6466,195],{"class":194},[165,6468,6469],{"class":167,"line":243},[165,6470,784],{"class":194},[165,6472,6473],{"class":167,"line":249},[165,6474,240],{"emptyLinePlaceholder":56},[165,6476,6477,6479,6482,6484,6486,6488,6490,6492,6494,6496],{"class":167,"line":295},[165,6478,2783],{"class":171},[165,6480,6481],{"class":303}," parseTimestamp",[165,6483,308],{"class":194},[165,6485,167],{"class":536},[165,6487,451],{"class":179},[165,6489,1310],{"class":748},[165,6491,343],{"class":194},[165,6493,451],{"class":179},[165,6495,3409],{"class":748},[165,6497,1297],{"class":194},[165,6499,6500,6502,6505,6507,6510,6512,6515,6517,6519,6523,6526,6529,6532,6534,6537,6539,6541,6543,6545,6548,6550,6552],{"class":167,"line":348},[165,6501,356],{"class":171},[165,6503,6504],{"class":175}," match",[165,6506,180],{"class":179},[165,6508,6509],{"class":256}," line",[165,6511,46],{"class":194},[165,6513,6514],{"class":303},"match",[165,6516,308],{"class":307},[165,6518,931],{"class":183},[165,6520,6522],{"class":6521},"sjYin","\\[",[165,6524,308],{"class":6525},"s-KJb",[165,6527,6528],{"class":938},"\\d",[165,6530,6531],{"class":179},"+",[165,6533,343],{"class":6525},[165,6535,451],{"class":6536},"sQRbd",[165,6538,308],{"class":6525},[165,6540,6528],{"class":938},[165,6542,6531],{"class":179},[165,6544,343],{"class":6525},[165,6546,6547],{"class":6521},"\\]",[165,6549,931],{"class":183},[165,6551,343],{"class":307},[165,6553,195],{"class":194},[165,6555,6556,6558,6560,6562,6564],{"class":167,"line":353},[165,6557,589],{"class":252},[165,6559,257],{"class":307},[165,6561,6514],{"class":256},[165,6563,289],{"class":307},[165,6565,292],{"class":194},[165,6567,6568,6570,6572,6574,6577,6579,6581,6583,6585,6588],{"class":167,"line":373},[165,6569,604],{"class":171},[165,6571,6368],{"class":175},[165,6573,180],{"class":179},[165,6575,6576],{"class":303}," parseInt",[165,6578,308],{"class":307},[165,6580,6514],{"class":256},[165,6582,935],{"class":307},[165,6584,1734],{"class":226},[165,6586,6587],{"class":307},"])",[165,6589,195],{"class":194},[165,6591,6592,6594,6597,6599,6601,6603,6605,6607,6609,6611],{"class":167,"line":387},[165,6593,604],{"class":171},[165,6595,6596],{"class":175}," seconds",[165,6598,180],{"class":179},[165,6600,6576],{"class":303},[165,6602,308],{"class":307},[165,6604,6514],{"class":256},[165,6606,935],{"class":307},[165,6608,1685],{"class":226},[165,6610,6587],{"class":307},[165,6612,195],{"class":194},[165,6614,6615,6617,6619,6622,6624,6626,6628],{"class":167,"line":401},[165,6616,4360],{"class":252},[165,6618,6368],{"class":256},[165,6620,6621],{"class":179}," *",[165,6623,6388],{"class":226},[165,6625,323],{"class":179},[165,6627,6596],{"class":256},[165,6629,195],{"class":194},[165,6631,6632],{"class":167,"line":413},[165,6633,725],{"class":194},[165,6635,6636,6638,6640],{"class":167,"line":421},[165,6637,3341],{"class":252},[165,6639,269],{"class":226},[165,6641,195],{"class":194},[165,6643,6644],{"class":167,"line":426},[165,6645,784],{"class":194},[148,6647,6649],{"id":6648},"_3-data-transformation-pipeline","3. Data Transformation Pipeline",[15,6651,6652],{},"The extraction process needed to:",[102,6654,6655,6658,6661,6664,6667],{},[105,6656,6657],{},"Read the raw JSON transcript",[105,6659,6660],{},"Filter by timestamp range",[105,6662,6663],{},"Format each dialogue entry",[105,6665,6666],{},"Maintain chronological order",[105,6668,6669],{},"Output in a readable format",[156,6671,6673],{"className":158,"code":6672,"language":160,"meta":48,"style":48},"const relevantDialogue: string[] = [];\n\ntranscriptCues.forEach((cue: any) => {\n  Object.values(cue).forEach((entry: any) => {\n    if (entry.start_time >= START_TIME && entry.end_time \u003C= END_TIME) {\n      relevantDialogue.push(\n        `[${formatTimestamp(entry.start_time)}] ${entry.speaker_name}:\\n${\n          entry.text\n        }\\n`,\n      );\n    }\n  });\n});\n\n\u002F\u002F Sort by start_time to ensure chronological order\nrelevantDialogue.sort((a, b) => {\n  const timeA = parseTimestamp(a);\n  const timeB = parseTimestamp(b);\n  return timeA - timeB;\n});\n",[162,6674,6675,6694,6698,6724,6761,6802,6814,6858,6868,6879,6886,6891,6899,6907,6911,6916,6943,6962,6982,6995],{"__ignoreMap":48},[165,6676,6677,6679,6682,6684,6686,6688,6690,6692],{"class":167,"line":168},[165,6678,172],{"class":171},[165,6680,6681],{"class":175}," relevantDialogue",[165,6683,451],{"class":179},[165,6685,1310],{"class":748},[165,6687,2845],{"class":256},[165,6689,266],{"class":179},[165,6691,3748],{"class":256},[165,6693,195],{"class":194},[165,6695,6696],{"class":167,"line":49},[165,6697,240],{"emptyLinePlaceholder":56},[165,6699,6700,6703,6705,6707,6709,6711,6714,6716,6718,6720,6722],{"class":167,"line":216},[165,6701,6702],{"class":256},"transcriptCues",[165,6704,46],{"class":194},[165,6706,4473],{"class":303},[165,6708,308],{"class":256},[165,6710,308],{"class":194},[165,6712,6713],{"class":536},"cue",[165,6715,451],{"class":179},[165,6717,2721],{"class":748},[165,6719,343],{"class":194},[165,6721,761],{"class":171},[165,6723,1297],{"class":194},[165,6725,6726,6729,6731,6734,6736,6738,6740,6742,6744,6746,6748,6751,6753,6755,6757,6759],{"class":167,"line":237},[165,6727,6728],{"class":256},"  Object",[165,6730,46],{"class":194},[165,6732,6733],{"class":303},"values",[165,6735,308],{"class":307},[165,6737,6713],{"class":256},[165,6739,343],{"class":307},[165,6741,46],{"class":194},[165,6743,4473],{"class":303},[165,6745,308],{"class":307},[165,6747,308],{"class":194},[165,6749,6750],{"class":536},"entry",[165,6752,451],{"class":179},[165,6754,2721],{"class":748},[165,6756,343],{"class":194},[165,6758,761],{"class":171},[165,6760,1297],{"class":194},[165,6762,6763,6766,6768,6770,6772,6775,6778,6781,6784,6787,6789,6792,6795,6798,6800],{"class":167,"line":243},[165,6764,6765],{"class":252},"    if",[165,6767,257],{"class":307},[165,6769,6750],{"class":256},[165,6771,46],{"class":194},[165,6773,6774],{"class":256},"start_time",[165,6776,6777],{"class":179}," >=",[165,6779,6780],{"class":175}," START_TIME",[165,6782,6783],{"class":179}," &&",[165,6785,6786],{"class":256}," entry",[165,6788,46],{"class":194},[165,6790,6791],{"class":256},"end_time",[165,6793,6794],{"class":179}," \u003C=",[165,6796,6797],{"class":175}," END_TIME",[165,6799,289],{"class":307},[165,6801,292],{"class":194},[165,6803,6804,6807,6809,6812],{"class":167,"line":249},[165,6805,6806],{"class":256},"      relevantDialogue",[165,6808,46],{"class":194},[165,6810,6811],{"class":303},"push",[165,6813,370],{"class":307},[165,6815,6816,6819,6821,6823,6826,6828,6830,6832,6834,6836,6838,6840,6842,6844,6846,6849,6851,6853,6855],{"class":167,"line":295},[165,6817,6818],{"class":183},"        `",[165,6820,935],{"class":187},[165,6822,317],{"class":183},[165,6824,6825],{"class":303},"formatTimestamp",[165,6827,308],{"class":1659},[165,6829,6750],{"class":256},[165,6831,46],{"class":183},[165,6833,6774],{"class":256},[165,6835,343],{"class":1659},[165,6837,329],{"class":183},[165,6839,1475],{"class":187},[165,6841,317],{"class":183},[165,6843,6750],{"class":256},[165,6845,46],{"class":183},[165,6847,6848],{"class":256},"speaker_name",[165,6850,329],{"class":183},[165,6852,451],{"class":187},[165,6854,691],{"class":175},[165,6856,6857],{"class":183},"${\n",[165,6859,6860,6863,6865],{"class":167,"line":348},[165,6861,6862],{"class":256},"          entry",[165,6864,46],{"class":183},[165,6866,6867],{"class":256},"text\n",[165,6869,6870,6873,6875,6877],{"class":167,"line":353},[165,6871,6872],{"class":183},"        }",[165,6874,691],{"class":175},[165,6876,311],{"class":183},[165,6878,384],{"class":194},[165,6880,6881,6884],{"class":167,"line":373},[165,6882,6883],{"class":307},"      )",[165,6885,195],{"class":194},[165,6887,6888],{"class":167,"line":387},[165,6889,6890],{"class":194},"    }\n",[165,6892,6893,6895,6897],{"class":167,"line":401},[165,6894,492],{"class":194},[165,6896,343],{"class":307},[165,6898,195],{"class":194},[165,6900,6901,6903,6905],{"class":167,"line":413},[165,6902,329],{"class":194},[165,6904,343],{"class":256},[165,6906,195],{"class":194},[165,6908,6909],{"class":167,"line":421},[165,6910,240],{"emptyLinePlaceholder":56},[165,6912,6913],{"class":167,"line":426},[165,6914,6915],{"class":233},"\u002F\u002F Sort by start_time to ensure chronological order\n",[165,6917,6918,6921,6923,6926,6928,6930,6932,6934,6937,6939,6941],{"class":167,"line":445},[165,6919,6920],{"class":256},"relevantDialogue",[165,6922,46],{"class":194},[165,6924,6925],{"class":303},"sort",[165,6927,308],{"class":256},[165,6929,308],{"class":194},[165,6931,34],{"class":536},[165,6933,682],{"class":194},[165,6935,6936],{"class":536}," b",[165,6938,343],{"class":194},[165,6940,761],{"class":171},[165,6942,1297],{"class":194},[165,6944,6945,6947,6950,6952,6954,6956,6958,6960],{"class":167,"line":472},[165,6946,356],{"class":171},[165,6948,6949],{"class":175}," timeA",[165,6951,180],{"class":179},[165,6953,6481],{"class":303},[165,6955,308],{"class":307},[165,6957,34],{"class":256},[165,6959,343],{"class":307},[165,6961,195],{"class":194},[165,6963,6964,6966,6969,6971,6973,6975,6978,6980],{"class":167,"line":489},[165,6965,356],{"class":171},[165,6967,6968],{"class":175}," timeB",[165,6970,180],{"class":179},[165,6972,6481],{"class":303},[165,6974,308],{"class":307},[165,6976,6977],{"class":256},"b",[165,6979,343],{"class":307},[165,6981,195],{"class":194},[165,6983,6984,6986,6988,6991,6993],{"class":167,"line":499},[165,6985,3341],{"class":252},[165,6987,6949],{"class":256},[165,6989,6990],{"class":179}," -",[165,6992,6968],{"class":256},[165,6994,195],{"class":194},[165,6996,6997,6999,7001],{"class":167,"line":504},[165,6998,329],{"class":194},[165,7000,343],{"class":256},[165,7002,195],{"class":194},[148,7004,7006],{"id":7005},"_4-marketing-ready-output","4. Marketing-Ready Output",[15,7008,7009],{},"The final output needed to serve multiple purposes:",[1070,7011,7012,7015,7018,7021],{},[105,7013,7014],{},"Human-readable Q&A format for direct analysis",[105,7016,7017],{},"Structured format for LLM processing",[105,7019,7020],{},"Clean text that could be used in marketing materials",[105,7022,7023],{},"Preserved context while maintaining privacy",[15,7025,7026],{},"Example of the extracted dialogue:",[156,7028,7033],{"className":7029,"code":7031,"language":7032,"meta":48},[7030],"language-text","[52:24] Interviewer:\nWhat is your motivation to want to learn how to use the platform? And I ask this because in my experience with dealing with our other customers, it's normally the users that have a background in engineering that really want to get into the platform and learn how to use it and enjoy using it.\n\n[52:55] Customer:\nWell, first, we don't have a lot of money in the company... I was a test coordinator... So I'm interested in it. I just, well, I'm, I like working in it.\n","text",[162,7034,7031],{"__ignoreMap":48},[19,7036,1048],{"id":1047},[102,7038,7039,7055,7074],{},[105,7040,7041,7044],{},[794,7042,7043],{},"Network Analysis for Data Discovery",[1070,7045,7046,7049,7052],{},[105,7047,7048],{},"Browser DevTools are invaluable for understanding data flow",[105,7050,7051],{},"Complex UIs often have structured data underneath",[105,7053,7054],{},"Look for patterns in request\u002Fresponse cycles",[105,7056,7057,7060],{},[794,7058,7059],{},"Data Transformation Strategy",[1070,7061,7062,7065,7068,7071],{},[105,7063,7064],{},"Start with the end format and work backwards",[105,7066,7067],{},"Build modular transformation functions",[105,7069,7070],{},"Handle edge cases (timestamp formats, sorting, etc.)",[105,7072,7073],{},"Maintain data fidelity while cleaning",[105,7075,7076,7079],{},[794,7077,7078],{},"Privacy-First Processing",[1070,7080,7081,7084,7087],{},[105,7082,7083],{},"Remove sensitive information early in the pipeline",[105,7085,7086],{},"Create reusable anonymization patterns",[105,7088,7089],{},"Preserve context while protecting privacy",[19,7091,7093],{"id":7092},"business-impact","Business Impact",[15,7095,7096],{},"The automation delivered several key benefits:",[102,7098,7099,7115,7131],{},[105,7100,7101,7104],{},[794,7102,7103],{},"Marketing Insights",[1070,7105,7106,7109,7112],{},[105,7107,7108],{},"Identified key user motivations and pain points",[105,7110,7111],{},"Captured authentic customer voice and terminology",[105,7113,7114],{},"Discovered unexpected use cases and value propositions",[105,7116,7117,7120],{},[794,7118,7119],{},"ICP Development",[1070,7121,7122,7125,7128],{},[105,7123,7124],{},"Refined ideal customer characteristics",[105,7126,7127],{},"Validated existing personas",[105,7129,7130],{},"Uncovered new customer segments",[105,7132,7133,7136],{},[794,7134,7135],{},"Content Creation",[1070,7137,7138,7141,7144],{},[105,7139,7140],{},"Generated authentic marketing narratives",[105,7142,7143],{},"Created customer success stories",[105,7145,7146],{},"Developed more targeted messaging",[19,7148,1119],{"id":1118},[102,7150,7151,7167,7183],{},[105,7152,7153,7156],{},[794,7154,7155],{},"Automation Expansion",[1070,7157,7158,7161,7164],{},[105,7159,7160],{},"Process multiple interview transcripts in batch",[105,7162,7163],{},"Add more output formats for different uses",[105,7165,7166],{},"Create templates for common marketing outputs",[105,7168,7169,7172],{},[794,7170,7171],{},"Analysis Enhancement",[1070,7173,7174,7177,7180],{},[105,7175,7176],{},"Add sentiment analysis",[105,7178,7179],{},"Track topic frequencies",[105,7181,7182],{},"Generate word clouds and key phrase extraction",[105,7184,7185,7188],{},[794,7186,7187],{},"Integration Opportunities",[1070,7189,7190,7193,7196],{},[105,7191,7192],{},"Connect with CRM for customer insight tracking",[105,7194,7195],{},"Integrate with content management systems",[105,7197,7198],{},"Build a library of customer insights",[19,7200,1129],{"id":1128},[15,7202,7203],{},"Dang, this project showed me how sometimes technical skills can create value in unexpected places. What started as a simple transcript extraction turned into a tool for marketing insight generation. It showed methat sometimes the most valuable data is already in our possession - we just need creative ways to access and transform it. 🙌",[15,7205,7206,7207],{},"The combination of network analysis, data transformation, and LLM processing created a repeatable process for turning customer conversations into actionable marketing insights. ",[83,7208,7209],{},"Ofcourse, while respecting privacy and maintaining data quality.",[1154,7211],{},[15,7213,7214],{},[83,7215,7216],{},"Note: This case study focuses on the technical implementation while respecting confidentiality around specific customer conversations and platform details.",[1162,7218,7219],{},"html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sucvu, html code.shiki .sucvu{--shiki-light:#E53935;--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sfo-9, html code.shiki .sfo-9{--shiki-light:#90A4AE;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sjYin, html code.shiki .sjYin{--shiki-light:#90A4AE;--shiki-light-font-weight:inherit;--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .s-KJb, html code.shiki .s-KJb{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .stzsN, html code.shiki .stzsN{--shiki-light:#91B859;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sQRbd, html code.shiki .sQRbd{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}",{"title":48,"searchDepth":49,"depth":49,"links":7221},[7222,7223,7224,7230,7231,7232,7233],{"id":77,"depth":49,"text":78},{"id":125,"depth":49,"text":126},{"id":1239,"depth":49,"text":1240,"children":7225},[7226,7227,7228,7229],{"id":6202,"depth":216,"text":6203},{"id":6328,"depth":216,"text":6329},{"id":6648,"depth":216,"text":6649},{"id":7005,"depth":216,"text":7006},{"id":1047,"depth":49,"text":1048},{"id":7092,"depth":49,"text":7093},{"id":1118,"depth":49,"text":1119},{"id":1128,"depth":49,"text":1129},"2024-07-14","Built a transcript extraction pipeline that turns customer conversations into actionable marketing insights by combining network request analysis, data transformation, and LLM processing.",{"slug":7237},"video-transcript-analysis-automation","\u002Fwork\u002Fvideo-transcript-analysis-automation",{"title":6158,"description":7235},"work\u002Fvideo-transcript-analysis-automation",[64,7242,7243,1262,7244],"Network Analysis","Data Extraction","Marketing","h2NqeR8kXmlnMGokqUTZec85FRN8Lxgl99hDqsSMJEY",{"id":7247,"title":7248,"body":7249,"date":8416,"description":8417,"extension":55,"externalUrl":1180,"featured":56,"kind":1182,"meta":8418,"navigation":56,"path":8420,"seo":8421,"stem":8422,"tags":8423,"__hash__":8428},"work\u002Fwork\u002Frebuilding-capptions-website.md","Rebuilding capptions.com - with next.js",{"type":8,"value":7250,"toc":8404},[7251,7254,7261,7263,7266,7269,7271,7274,7306,7308,7312,7441,7445,7458,7461,7846,7853,7856,8048,8052,8059,8062,8189,8195,8198,8304,8306,8338,8341,8344,8369,8373,8376,8393,8398,8401],[11,7252,7248],{"id":7253},"rebuilding-capptionscom-with-nextjs",[15,7255,7256],{},[7257,7258],"img",{"alt":7259,"src":7260},"The Capptions.com homepage I designed and built","\u002Fimages\u002Fcontent\u002Fcapptions-home.webp",[19,7262,78],{"id":77},[15,7264,7265],{},"My journey to rebuilding Capptions.com started in an unexpected place: creating internal automation tools. As the Director of Marketing but also a passionate self-taught-developer at Capptions, I had been working on streamlining our internal processes through small automation projects. These projects, combined with my self-driven completion of CS50 (Harvard University), The Odin Project, and the Full Stack Open (FSO - University of Helsinki) courses, caught our CTO's attention. When the opportunity arose to rebuild our marketing website, I saw it as a chance to apply modern web development practices at scale.",[15,7267,7268],{},"The existing website was outdated and bloated – it wasn't serving our core audience of EHS (Environmental, Health, and Safety) directors and managers effectively. We needed a platform that could evolve quickly with our product offerings and market positioning, while maintaining top-tier performance and SEO standards.",[19,7270,126],{"id":125},[15,7272,7273],{},"I created a modern, component-driven website using Next.js 14, with a focus on Server Components and optimal performance. The site serves as both our marketing platform and a technical showcase of our capabilities. Here's what makes it special:",[1070,7275,7276,7282,7288,7294,7300],{},[105,7277,7278,7281],{},[794,7279,7280],{},"Component Library",": A comprehensive collection of 50+ reusable components, from simple UI elements to complex interactive features",[105,7283,7284,7287],{},[794,7285,7286],{},"Performance-First Architecture",": Leveraging Next.js Server Components and static generation where possible",[105,7289,7290,7293],{},[794,7291,7292],{},"Content Management",": Integration with Sanity CMS (though that's a story for another post!)",[105,7295,7296,7299],{},[794,7297,7298],{},"Design System",": Custom implementation using TailwindCSS and ShadcnUI",[105,7301,7302,7305],{},[794,7303,7304],{},"SEO Optimization",": Built-in SEO features with dynamic meta tags and structured data",[19,7307,1240],{"id":1239},[148,7309,7311],{"id":7310},"stack-selection","Stack Selection",[156,7313,7315],{"className":158,"code":7314,"language":160,"meta":48,"style":48},"\u002F\u002F Example of our tech stack configuration\n{\n  framework: \"Next.js 14\",\n  styling: \"TailwindCSS\",\n  ui: \"ShadCN + Custom Components\",\n  deployment: \"Vercel\",\n  cms: \"Sanity\",\n  analytics: \"Posthog\",\n  typeChecking: \"TypeScript\",\n}\n",[162,7316,7317,7322,7326,7342,7358,7374,7390,7406,7422,7437],{"__ignoreMap":48},[165,7318,7319],{"class":167,"line":168},[165,7320,7321],{"class":233},"\u002F\u002F Example of our tech stack configuration\n",[165,7323,7324],{"class":167,"line":49},[165,7325,292],{"class":194},[165,7327,7328,7331,7333,7335,7338,7340],{"class":167,"line":216},[165,7329,7330],{"class":1293},"  framework",[165,7332,451],{"class":194},[165,7334,184],{"class":183},[165,7336,7337],{"class":187},"Next.js 14",[165,7339,191],{"class":183},[165,7341,384],{"class":194},[165,7343,7344,7347,7349,7351,7354,7356],{"class":167,"line":237},[165,7345,7346],{"class":1293},"  styling",[165,7348,451],{"class":194},[165,7350,184],{"class":183},[165,7352,7353],{"class":187},"TailwindCSS",[165,7355,191],{"class":183},[165,7357,384],{"class":194},[165,7359,7360,7363,7365,7367,7370,7372],{"class":167,"line":243},[165,7361,7362],{"class":1293},"  ui",[165,7364,451],{"class":194},[165,7366,184],{"class":183},[165,7368,7369],{"class":187},"ShadCN + Custom Components",[165,7371,191],{"class":183},[165,7373,384],{"class":194},[165,7375,7376,7379,7381,7383,7386,7388],{"class":167,"line":249},[165,7377,7378],{"class":1293},"  deployment",[165,7380,451],{"class":194},[165,7382,184],{"class":183},[165,7384,7385],{"class":187},"Vercel",[165,7387,191],{"class":183},[165,7389,384],{"class":194},[165,7391,7392,7395,7397,7399,7402,7404],{"class":167,"line":295},[165,7393,7394],{"class":1293},"  cms",[165,7396,451],{"class":194},[165,7398,184],{"class":183},[165,7400,7401],{"class":187},"Sanity",[165,7403,191],{"class":183},[165,7405,384],{"class":194},[165,7407,7408,7411,7413,7415,7418,7420],{"class":167,"line":348},[165,7409,7410],{"class":1293},"  analytics",[165,7412,451],{"class":194},[165,7414,184],{"class":183},[165,7416,7417],{"class":187},"Posthog",[165,7419,191],{"class":183},[165,7421,384],{"class":194},[165,7423,7424,7427,7429,7431,7433,7435],{"class":167,"line":353},[165,7425,7426],{"class":1293},"  typeChecking",[165,7428,451],{"class":194},[165,7430,184],{"class":183},[165,7432,64],{"class":187},[165,7434,191],{"class":183},[165,7436,384],{"class":194},[165,7438,7439],{"class":167,"line":373},[165,7440,784],{"class":194},[148,7442,7444],{"id":7443},"key-architectural-decisions","Key Architectural Decisions",[102,7446,7447,7453],{},[105,7448,7449,7452],{},[794,7450,7451],{},"Folder Structure","\nI implemented a feature-based architecture that scales with our growing needs:",[105,7454,7455],{},[794,7456,7457],{},"Performance Optimizations",[15,7459,7460],{},"One of my proudest achievements was implementing efficient component loading:",[156,7462,7464],{"className":158,"code":7463,"language":160,"meta":48,"style":48},"\u002F\u002F components\u002Flazy-loading-wrapper.tsx\nexport const LazyLoadingWrapper = ({ children, threshold = 0.1 }) => {\n  const [isVisible, setIsVisible] = useState(false);\n  const ref = useRef(null);\n\n  useEffect(() => {\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting) {\n          setIsVisible(true);\n          observer.disconnect();\n        }\n      },\n      { threshold },\n    );\n\n    if (ref.current) {\n      observer.observe(ref.current);\n    }\n\n    return () => observer.disconnect();\n  }, [threshold]);\n\n  return (\n    \u003Cdiv ref={ref}>\n      {isVisible ? children : \u003Cdiv className=\"h-[200px] animate-pulse\" \u002F>}\n    \u003C\u002Fdiv>\n  );\n};\n",[162,7465,7466,7471,7507,7537,7558,7562,7575,7591,7604,7622,7636,7650,7655,7659,7667,7673,7677,7695,7717,7721,7725,7744,7758,7762,7769,7790,7827,7836,7842],{"__ignoreMap":48},[165,7467,7468],{"class":167,"line":168},[165,7469,7470],{"class":233},"\u002F\u002F components\u002Flazy-loading-wrapper.tsx\n",[165,7472,7473,7476,7479,7482,7484,7487,7490,7492,7495,7497,7500,7503,7505],{"class":167,"line":49},[165,7474,7475],{"class":252},"export",[165,7477,7478],{"class":171}," const",[165,7480,7481],{"class":5661}," LazyLoadingWrapper",[165,7483,180],{"class":179},[165,7485,7486],{"class":194}," ({",[165,7488,7489],{"class":536}," children",[165,7491,682],{"class":194},[165,7493,7494],{"class":536}," threshold",[165,7496,180],{"class":179},[165,7498,7499],{"class":226}," 0.1",[165,7501,7502],{"class":194}," })",[165,7504,761],{"class":171},[165,7506,1297],{"class":194},[165,7508,7509,7511,7513,7516,7518,7521,7523,7525,7528,7530,7533,7535],{"class":167,"line":216},[165,7510,356],{"class":171},[165,7512,5839],{"class":194},[165,7514,7515],{"class":175},"isVisible",[165,7517,682],{"class":194},[165,7519,7520],{"class":175}," setIsVisible",[165,7522,942],{"class":194},[165,7524,180],{"class":179},[165,7526,7527],{"class":303}," useState",[165,7529,308],{"class":307},[165,7531,7532],{"class":5797},"false",[165,7534,343],{"class":307},[165,7536,195],{"class":194},[165,7538,7539,7541,7544,7546,7549,7551,7554,7556],{"class":167,"line":237},[165,7540,356],{"class":171},[165,7542,7543],{"class":175}," ref",[165,7545,180],{"class":179},[165,7547,7548],{"class":303}," useRef",[165,7550,308],{"class":307},[165,7552,7553],{"class":934},"null",[165,7555,343],{"class":307},[165,7557,195],{"class":194},[165,7559,7560],{"class":167,"line":243},[165,7561,240],{"emptyLinePlaceholder":56},[165,7563,7564,7567,7569,7571,7573],{"class":167,"line":249},[165,7565,7566],{"class":303},"  useEffect",[165,7568,308],{"class":307},[165,7570,914],{"class":194},[165,7572,761],{"class":171},[165,7574,1297],{"class":194},[165,7576,7577,7579,7582,7584,7586,7589],{"class":167,"line":295},[165,7578,604],{"class":171},[165,7580,7581],{"class":175}," observer",[165,7583,180],{"class":179},[165,7585,745],{"class":179},[165,7587,7588],{"class":303}," IntersectionObserver",[165,7590,370],{"class":307},[165,7592,7593,7596,7598,7600,7602],{"class":167,"line":348},[165,7594,7595],{"class":194},"      ([",[165,7597,6750],{"class":536},[165,7599,6587],{"class":194},[165,7601,761],{"class":171},[165,7603,1297],{"class":194},[165,7605,7606,7609,7611,7613,7615,7618,7620],{"class":167,"line":353},[165,7607,7608],{"class":252},"        if",[165,7610,257],{"class":307},[165,7612,6750],{"class":256},[165,7614,46],{"class":194},[165,7616,7617],{"class":256},"isIntersecting",[165,7619,289],{"class":307},[165,7621,292],{"class":194},[165,7623,7624,7627,7629,7632,7634],{"class":167,"line":373},[165,7625,7626],{"class":303},"          setIsVisible",[165,7628,308],{"class":307},[165,7630,7631],{"class":5797},"true",[165,7633,343],{"class":307},[165,7635,195],{"class":194},[165,7637,7638,7641,7643,7646,7648],{"class":167,"line":387},[165,7639,7640],{"class":256},"          observer",[165,7642,46],{"class":194},[165,7644,7645],{"class":303},"disconnect",[165,7647,914],{"class":307},[165,7649,195],{"class":194},[165,7651,7652],{"class":167,"line":401},[165,7653,7654],{"class":194},"        }\n",[165,7656,7657],{"class":167,"line":413},[165,7658,5567],{"class":194},[165,7660,7661,7663,7665],{"class":167,"line":421},[165,7662,1904],{"class":194},[165,7664,7494],{"class":256},[165,7666,1931],{"class":194},[165,7668,7669,7671],{"class":167,"line":426},[165,7670,2931],{"class":307},[165,7672,195],{"class":194},[165,7674,7675],{"class":167,"line":445},[165,7676,240],{"emptyLinePlaceholder":56},[165,7678,7679,7681,7683,7686,7688,7691,7693],{"class":167,"line":472},[165,7680,6765],{"class":252},[165,7682,257],{"class":307},[165,7684,7685],{"class":256},"ref",[165,7687,46],{"class":194},[165,7689,7690],{"class":256},"current",[165,7692,289],{"class":307},[165,7694,292],{"class":194},[165,7696,7697,7700,7702,7705,7707,7709,7711,7713,7715],{"class":167,"line":489},[165,7698,7699],{"class":256},"      observer",[165,7701,46],{"class":194},[165,7703,7704],{"class":303},"observe",[165,7706,308],{"class":307},[165,7708,7685],{"class":256},[165,7710,46],{"class":194},[165,7712,7690],{"class":256},[165,7714,343],{"class":307},[165,7716,195],{"class":194},[165,7718,7719],{"class":167,"line":499},[165,7720,6890],{"class":194},[165,7722,7723],{"class":167,"line":504},[165,7724,240],{"emptyLinePlaceholder":56},[165,7726,7727,7729,7732,7734,7736,7738,7740,7742],{"class":167,"line":510},[165,7728,4360],{"class":252},[165,7730,7731],{"class":194}," ()",[165,7733,761],{"class":171},[165,7735,7581],{"class":256},[165,7737,46],{"class":194},[165,7739,7645],{"class":303},[165,7741,914],{"class":307},[165,7743,195],{"class":194},[165,7745,7746,7749,7751,7754,7756],{"class":167,"line":545},[165,7747,7748],{"class":194},"  },",[165,7750,5839],{"class":307},[165,7752,7753],{"class":256},"threshold",[165,7755,6587],{"class":307},[165,7757,195],{"class":194},[165,7759,7760],{"class":167,"line":574},[165,7761,240],{"emptyLinePlaceholder":56},[165,7763,7764,7766],{"class":167,"line":581},[165,7765,3341],{"class":252},[165,7767,7768],{"class":307}," (\n",[165,7770,7771,7774,7777,7779,7781,7783,7785,7787],{"class":167,"line":586},[165,7772,7773],{"class":179},"    \u003C",[165,7775,7776],{"class":256},"div",[165,7778,7543],{"class":256},[165,7780,266],{"class":179},[165,7782,1611],{"class":194},[165,7784,7685],{"class":256},[165,7786,329],{"class":194},[165,7788,7789],{"class":179},">\n",[165,7791,7792,7794,7796,7799,7802,7805,7808,7810,7813,7815,7817,7820,7822,7825],{"class":167,"line":601},[165,7793,1904],{"class":194},[165,7795,7515],{"class":536},[165,7797,7798],{"class":307}," ? ",[165,7800,7801],{"class":1321},"children",[165,7803,7804],{"class":194}," :",[165,7806,7807],{"class":307}," \u003C",[165,7809,7776],{"class":536},[165,7811,7812],{"class":536}," className",[165,7814,266],{"class":179},[165,7816,191],{"class":183},[165,7818,7819],{"class":187},"h-[200px] animate-pulse",[165,7821,191],{"class":183},[165,7823,7824],{"class":179}," \u002F>",[165,7826,784],{"class":194},[165,7828,7829,7832,7834],{"class":167,"line":632},[165,7830,7831],{"class":179},"    \u003C\u002F",[165,7833,7776],{"class":256},[165,7835,7789],{"class":179},[165,7837,7838,7840],{"class":167,"line":650},[165,7839,416],{"class":307},[165,7841,195],{"class":194},[165,7843,7844],{"class":167,"line":655},[165,7845,1515],{"class":194},[102,7847,7848],{"start":216},[105,7849,7850],{},[794,7851,7852],{},"Component Architecture",[15,7854,7855],{},"I developed a system of composable components that could be mixed and matched for different page layouts. Here's an example of our hero component structure:",[156,7857,7859],{"className":158,"code":7858,"language":160,"meta":48,"style":48},"\u002F\u002F Simplified version of our hero component architecture\ninterface HeroProps {\n  variant: \"default\" | \"centered\" | \"split\";\n  title: string;\n  description: string;\n  cta?: {\n    text: string;\n    href: string;\n  };\n}\n\nexport const Hero: React.FC\u003CHeroProps> = ({\n  variant,\n  title,\n  description,\n  cta,\n}) => {\n  \u002F\u002F Component logic\n};\n",[162,7860,7861,7866,7875,7909,7919,7930,7939,7950,7961,7965,7969,7973,8005,8011,8017,8023,8029,8039,8044],{"__ignoreMap":48},[165,7862,7863],{"class":167,"line":168},[165,7864,7865],{"class":233},"\u002F\u002F Simplified version of our hero component architecture\n",[165,7867,7868,7870,7873],{"class":167,"line":49},[165,7869,1290],{"class":171},[165,7871,7872],{"class":1293}," HeroProps",[165,7874,1297],{"class":194},[165,7876,7877,7880,7882,7884,7887,7889,7891,7893,7896,7898,7900,7902,7905,7907],{"class":167,"line":216},[165,7878,7879],{"class":1321},"  variant",[165,7881,451],{"class":179},[165,7883,184],{"class":183},[165,7885,7886],{"class":187},"default",[165,7888,191],{"class":183},[165,7890,2078],{"class":179},[165,7892,184],{"class":183},[165,7894,7895],{"class":187},"centered",[165,7897,191],{"class":183},[165,7899,2078],{"class":179},[165,7901,184],{"class":183},[165,7903,7904],{"class":187},"split",[165,7906,191],{"class":183},[165,7908,195],{"class":194},[165,7910,7911,7913,7915,7917],{"class":167,"line":237},[165,7912,5079],{"class":1321},[165,7914,451],{"class":179},[165,7916,1310],{"class":748},[165,7918,195],{"class":194},[165,7920,7921,7924,7926,7928],{"class":167,"line":243},[165,7922,7923],{"class":1321},"  description",[165,7925,451],{"class":179},[165,7927,1310],{"class":748},[165,7929,195],{"class":194},[165,7931,7932,7935,7937],{"class":167,"line":249},[165,7933,7934],{"class":1321},"  cta",[165,7936,2104],{"class":179},[165,7938,1297],{"class":194},[165,7940,7941,7944,7946,7948],{"class":167,"line":295},[165,7942,7943],{"class":1321},"    text",[165,7945,451],{"class":179},[165,7947,1310],{"class":748},[165,7949,195],{"class":194},[165,7951,7952,7955,7957,7959],{"class":167,"line":348},[165,7953,7954],{"class":1321},"    href",[165,7956,451],{"class":179},[165,7958,1310],{"class":748},[165,7960,195],{"class":194},[165,7962,7963],{"class":167,"line":353},[165,7964,1344],{"class":194},[165,7966,7967],{"class":167,"line":373},[165,7968,784],{"class":194},[165,7970,7971],{"class":167,"line":387},[165,7972,240],{"emptyLinePlaceholder":56},[165,7974,7975,7977,7979,7982,7984,7987,7989,7992,7994,7997,7999,8001,8003],{"class":167,"line":401},[165,7976,7475],{"class":252},[165,7978,7478],{"class":171},[165,7980,7981],{"class":5661}," Hero",[165,7983,451],{"class":179},[165,7985,7986],{"class":1293}," React",[165,7988,46],{"class":194},[165,7990,7991],{"class":1293},"FC",[165,7993,276],{"class":194},[165,7995,7996],{"class":1293},"HeroProps",[165,7998,3432],{"class":194},[165,8000,180],{"class":179},[165,8002,257],{"class":256},[165,8004,292],{"class":194},[165,8006,8007,8009],{"class":167,"line":413},[165,8008,7879],{"class":256},[165,8010,384],{"class":194},[165,8012,8013,8015],{"class":167,"line":421},[165,8014,5079],{"class":256},[165,8016,384],{"class":194},[165,8018,8019,8021],{"class":167,"line":426},[165,8020,7923],{"class":256},[165,8022,384],{"class":194},[165,8024,8025,8027],{"class":167,"line":445},[165,8026,7934],{"class":256},[165,8028,384],{"class":194},[165,8030,8031,8033,8035,8037],{"class":167,"line":472},[165,8032,329],{"class":194},[165,8034,289],{"class":256},[165,8036,5706],{"class":171},[165,8038,1297],{"class":194},[165,8040,8041],{"class":167,"line":489},[165,8042,8043],{"class":233},"  \u002F\u002F Component logic\n",[165,8045,8046],{"class":167,"line":499},[165,8047,1515],{"class":194},[148,8049,8051],{"id":8050},"challenges-and-solutions","Challenges and Solutions",[102,8053,8054],{},[105,8055,8056],{},[794,8057,8058],{},"Performance vs. Rich Interactions",[15,8060,8061],{},"One of the biggest challenges was balancing rich interactive features with performance. I solved this through strategic code splitting and lazy loading:",[156,8063,8065],{"className":158,"code":8064,"language":160,"meta":48,"style":48},"\u002F\u002F Example of how we handle dynamic imports\nconst DynamicWorkflowBuilder = dynamic(\n  () =>\n    import(\"@\u002Fcomponents\u002Fworkflow-builder-drawer\").then(\n      (mod) => mod.WorkflowBuilder,\n    ),\n  {\n    loading: () => \u003CLoadingSpinner \u002F>,\n    ssr: false,\n  },\n);\n",[162,8066,8067,8072,8086,8093,8116,8138,8144,8148,8168,8179,8183],{"__ignoreMap":48},[165,8068,8069],{"class":167,"line":168},[165,8070,8071],{"class":233},"\u002F\u002F Example of how we handle dynamic imports\n",[165,8073,8074,8076,8079,8081,8084],{"class":167,"line":49},[165,8075,172],{"class":171},[165,8077,8078],{"class":175}," DynamicWorkflowBuilder",[165,8080,180],{"class":179},[165,8082,8083],{"class":303}," dynamic",[165,8085,370],{"class":256},[165,8087,8088,8091],{"class":167,"line":216},[165,8089,8090],{"class":194},"  ()",[165,8092,542],{"class":171},[165,8094,8095,8098,8100,8102,8105,8107,8109,8111,8114],{"class":167,"line":237},[165,8096,8097],{"class":179},"    import",[165,8099,308],{"class":256},[165,8101,191],{"class":183},[165,8103,8104],{"class":187},"@\u002Fcomponents\u002Fworkflow-builder-drawer",[165,8106,191],{"class":183},[165,8108,343],{"class":256},[165,8110,46],{"class":194},[165,8112,8113],{"class":303},"then",[165,8115,370],{"class":256},[165,8117,8118,8121,8124,8126,8128,8131,8133,8136],{"class":167,"line":243},[165,8119,8120],{"class":194},"      (",[165,8122,8123],{"class":536},"mod",[165,8125,343],{"class":194},[165,8127,761],{"class":171},[165,8129,8130],{"class":256}," mod",[165,8132,46],{"class":194},[165,8134,8135],{"class":256},"WorkflowBuilder",[165,8137,384],{"class":194},[165,8139,8140,8142],{"class":167,"line":249},[165,8141,2931],{"class":256},[165,8143,384],{"class":194},[165,8145,8146],{"class":167,"line":295},[165,8147,2854],{"class":194},[165,8149,8150,8153,8155,8157,8159,8161,8164,8166],{"class":167,"line":348},[165,8151,8152],{"class":303},"    loading",[165,8154,451],{"class":194},[165,8156,7731],{"class":194},[165,8158,761],{"class":171},[165,8160,7807],{"class":256},[165,8162,8163],{"class":1293},"LoadingSpinner",[165,8165,7824],{"class":256},[165,8167,384],{"class":194},[165,8169,8170,8173,8175,8177],{"class":167,"line":353},[165,8171,8172],{"class":307},"    ssr",[165,8174,451],{"class":194},[165,8176,5798],{"class":5797},[165,8178,384],{"class":194},[165,8180,8181],{"class":167,"line":373},[165,8182,1816],{"class":194},[165,8184,8185,8187],{"class":167,"line":387},[165,8186,343],{"class":256},[165,8188,195],{"class":194},[102,8190,8191],{"start":49},[105,8192,8193],{},[794,8194,7304],{},[15,8196,8197],{},"I implemented a robust SEO strategy using Next.js's metadata API:",[156,8199,8201],{"className":158,"code":8200,"language":160,"meta":48,"style":48},"\u002F\u002F app\u002F(main)\u002Flayout.tsx\nexport const metadata: Metadata = {\n  metadataBase: new URL(\"https:\u002F\u002Fcapptions.com\"),\n  title: {\n    default: \"Capptions - EHS & ESG Compliance Software\",\n    template: \"%s | Capptions\",\n  },\n  \u002F\u002F ... other metadata\n};\n",[162,8202,8203,8208,8226,8251,8259,8275,8291,8295,8300],{"__ignoreMap":48},[165,8204,8205],{"class":167,"line":168},[165,8206,8207],{"class":233},"\u002F\u002F app\u002F(main)\u002Flayout.tsx\n",[165,8209,8210,8212,8214,8217,8219,8222,8224],{"class":167,"line":49},[165,8211,7475],{"class":252},[165,8213,7478],{"class":171},[165,8215,8216],{"class":175}," metadata",[165,8218,451],{"class":179},[165,8220,8221],{"class":1293}," Metadata",[165,8223,180],{"class":179},[165,8225,1297],{"class":194},[165,8227,8228,8231,8233,8235,8238,8240,8242,8245,8247,8249],{"class":167,"line":216},[165,8229,8230],{"class":307},"  metadataBase",[165,8232,451],{"class":194},[165,8234,745],{"class":179},[165,8236,8237],{"class":303}," URL",[165,8239,308],{"class":256},[165,8241,191],{"class":183},[165,8243,8244],{"class":187},"https:\u002F\u002Fcapptions.com",[165,8246,191],{"class":183},[165,8248,343],{"class":256},[165,8250,384],{"class":194},[165,8252,8253,8255,8257],{"class":167,"line":237},[165,8254,5079],{"class":307},[165,8256,451],{"class":194},[165,8258,1297],{"class":194},[165,8260,8261,8264,8266,8268,8271,8273],{"class":167,"line":243},[165,8262,8263],{"class":307},"    default",[165,8265,451],{"class":194},[165,8267,184],{"class":183},[165,8269,8270],{"class":187},"Capptions - EHS & ESG Compliance Software",[165,8272,191],{"class":183},[165,8274,384],{"class":194},[165,8276,8277,8280,8282,8284,8287,8289],{"class":167,"line":249},[165,8278,8279],{"class":307},"    template",[165,8281,451],{"class":194},[165,8283,184],{"class":183},[165,8285,8286],{"class":187},"%s | Capptions",[165,8288,191],{"class":183},[165,8290,384],{"class":194},[165,8292,8293],{"class":167,"line":295},[165,8294,1816],{"class":194},[165,8296,8297],{"class":167,"line":348},[165,8298,8299],{"class":233},"  \u002F\u002F ... other metadata\n",[165,8301,8302],{"class":167,"line":353},[165,8303,1515],{"class":194},[19,8305,1048],{"id":1047},[102,8307,8308,8314,8320,8326,8332],{},[105,8309,8310,8313],{},[794,8311,8312],{},"Server Components Are Game-Changing","\nMoving from a client-heavy approach to Server Components dramatically improved our initial page load times and SEO capabilities.",[105,8315,8316,8319],{},[794,8317,8318],{},"Type Safety Pays Off","\nTypeScript's strict mode caught countless potential issues before they hit production. The initial investment in proper typing saved hours of debugging.",[105,8321,8322,8325],{},[794,8323,8324],{},"Component Architecture Evolution","\nI learned to start with smaller, more focused components and compose them into larger features, rather than building monolithic components that are hard to maintain.",[105,8327,8328,8331],{},[794,8329,8330],{},"Userflow Monitoring is Crucial","\nSetting up Posthog early helped us identify and fix userflow bottlenecks before they impacted users.",[105,8333,8334,8337],{},[794,8335,8336],{},"Performance Monitoring is Crucial","\nRunning Lighthouse tests on the website helped me identify and fix performance bottlenecks before they impacted users.",[19,8339,8340],{"id":1118},"What's Next",[15,8342,8343],{},"The website is never \"done\" – it's a living project that evolves with our business needs. Here's what's on the horizon:",[102,8345,8346,8352,8358,8363],{},[105,8347,8348,8351],{},[794,8349,8350],{},"A\u002FB Testing Infrastructure",": Building a system to test different messaging and layouts",[105,8353,8354,8357],{},[794,8355,8356],{},"Enhanced Personalization",": Implementing user-specific content based on industry, role, and use-case.",[105,8359,8360,8362],{},[794,8361,2392],{},": Continuing to optimize for Core Web Vitals",[105,8364,8365,8368],{},[794,8366,8367],{},"Internationalization",": Expanding our language support beyond English",[19,8370,8372],{"id":8371},"impact-and-results","Impact and Results",[15,8374,8375],{},"The rebuild has had significant business impact:",[1070,8377,8378,8381,8387,8390],{},[105,8379,8380],{},"90% improvement in Core Web Vitals scores",[105,8382,8383,8384],{},"10x faster deployment cycles ",[83,8385,8386],{},"(previously we were limited to only content changes, now we can rapidly deploy new features, pages, layouts etc.)",[105,8388,8389],{},"30% increase in lead generation form submissions",[105,8391,8392],{},"Significantly improved developer experience for content updates",[15,8394,8395],{},[83,8396,8397],{},"(I'm now able to make changes to the website without having to wait for an external developer to do it for me: as I am the developer)",[15,8399,8400],{},"Honestly, this project wasn't just about \"building a website\" – it was about creating a solid foundation for Capptions' digital presence that could scale and evolve. It's a strong (single handed) mix of design, content (visual and written), and development. The trust placed in me by our CTO to take on this project has led to even more opportunities to innovate and improve our technical infrastructure.",[1162,8402,8403],{},"html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sfCm-, html code.shiki .sfCm-{--shiki-light:#90A4AE;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s39Yj, html code.shiki .s39Yj{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sucvu, html code.shiki .sucvu{--shiki-light:#E53935;--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":48,"searchDepth":49,"depth":49,"links":8405},[8406,8407,8408,8413,8414,8415],{"id":77,"depth":49,"text":78},{"id":125,"depth":49,"text":126},{"id":1239,"depth":49,"text":1240,"children":8409},[8410,8411,8412],{"id":7310,"depth":216,"text":7311},{"id":7443,"depth":216,"text":7444},{"id":8050,"depth":216,"text":8051},{"id":1047,"depth":49,"text":1048},{"id":1118,"depth":49,"text":8340},{"id":8371,"depth":49,"text":8372},"2024-03-24","How I transformed our company's web presence by rebuilding Capptions.com from scratch using Next.js, focusing on performance, maintainability, and rapid iteration.",{"author":8419},"Ajoy Gonsalves","\u002Fwork\u002Frebuilding-capptions-website",{"title":7248,"description":8417},"work\u002Frebuilding-capptions-website",[8424,64,7353,8425,7385,8426,8427],"Next.js","ShadcnUI","Performance","SEO","QumHivbjZBJlaHlt3GzkVl4w3QoRquiU5KTya5tU7-c",{"id":8430,"title":8431,"body":8432,"date":8909,"description":8910,"extension":55,"externalUrl":1180,"featured":1181,"kind":1182,"meta":8911,"navigation":56,"path":8912,"seo":8913,"stem":8914,"tags":8915,"__hash__":8920},"work\u002Fwork\u002Fhubspot-nurture-flow-automation.md","Building a nurture flow for SaaS onboarding",{"type":8,"value":8433,"toc":8897},[8434,8437,8439,8442,8445,8448,8462,8464,8467,8484,8487,8497,8499,8501,8527,8529,8536,8543,8550,8557,8559,8593,8595,8775,8779,8782,8832,8836,8887,8890,8892],[11,8435,8431],{"id":8436},"building-a-nurture-flow-for-saas-onboarding",[19,8438,78],{"id":77},[15,8440,8441],{},"As both the Director of Marketing and (an aspiring) developer at Capptions, I noticed a gap in our user onboarding process. New users were receiving the same generic welcome emails regardless of their behavior or engagement level. This one-size-fits-all approach wasn't effectively guiding users through our platform's key features, particularly our marketplace and DIY workflow builder.",[15,8443,8444],{},"The challenge was clear: build an intelligent nurture flow that could adapt to user behavior, provide relevant guidance, and ultimately drive better activation rates. I wanted to create something that felt personal and helpful, not just another automated email sequence.",[15,8446,8447],{},"My constraints were:",[1070,8449,8450,8453,8456,8459],{},[105,8451,8452],{},"Needs to be built in HubSpot",[105,8454,8455],{},"Needs thorough input from the customer service manager (co-ownership: content + structure + flow)",[105,8457,8458],{},"Metrics to be tracked and optimized for = open rates, click through rates, and unsubscribe rates",[105,8460,8461],{},"Use the minimal triggers available based on our current Webapp x Hubspot integration",[19,8463,126],{"id":125},[15,8465,8466],{},"I developed a dynamic email nurture flow in HubSpot that:",[102,8468,8469,8472,8475,8478,8481],{},[105,8470,8471],{},"Adapts to user behavior with 5-7 personalized touchpoints",[105,8473,8474],{},"Segments users based on their engagement patterns (marketplace buyers vs. DIY users)",[105,8476,8477],{},"Tracks specific in-app actions (credit usage, workflow completion, form\u002Freport creation)",[105,8479,8480],{},"Triggers contextual help emails based on user progress",[105,8482,8483],{},"Automates CRM updates and internal notifications",[15,8485,8486],{},"The system uses HubSpot's workflow capabilities with our web app's integration to create a responsive onboarding experience.",[15,8488,8489],{},[7257,8490],{"src":8491,"alt":8492,"className":8493},"\u002Fimages\u002Fcontent\u002Fnurture-flow-minimap.webp","Nurture flow minimap",[8494,8495,8496],"rounded-lg","border-yellow-500","border-2",[19,8498,1240],{"id":1239},[148,8500,1244],{"id":1243},[1070,8502,8503,8509,8515,8521],{},[105,8504,8505,8508],{},[794,8506,8507],{},"HubSpot Workflows",": Core automation engine",[105,8510,8511,8514],{},[794,8512,8513],{},"Custom Web App Integration",": (near) Real-time user action tracking",[105,8516,8517,8520],{},[794,8518,8519],{},"Slack Integration",": Internal notifications",[105,8522,8523,8526],{},[794,8524,8525],{},"CRM Integration",": Contact management and segmentation",[148,8528,1273],{"id":1272},[102,8530,8531],{},[105,8532,8533],{},[794,8534,8535],{},"Use-case Based Branching Logic",[15,8537,8538],{},[7257,8539],{"src":8540,"alt":8541,"className":8542},"\u002Fimages\u002Fcontent\u002Fuse-case-based-branching.webp","Use-case based branching logic",[8494,8495,8496],[102,8544,8545],{"start":49},[105,8546,8547],{},[794,8548,8549],{},"Usage Based Branching Logic",[15,8551,8552],{},[7257,8553],{"src":8554,"alt":8555,"className":8556},"\u002Fimages\u002Fcontent\u002Fusage-based-branching.webp","Usage based branching logic",[8494,8495,8496],[148,8558,4637],{"id":4636},[102,8560,8561,8577],{},[105,8562,8563,8566],{},[794,8564,8565],{},"Multiple User Paths",[1070,8567,8568,8571,8574],{},[105,8569,8570],{},"Marketplace vs. DIY user detection",[105,8572,8573],{},"Credit usage tracking",[105,8575,8576],{},"Workflow completion status",[105,8578,8579,8582],{},[794,8580,8581],{},"Email Frequency Control",[1070,8583,8584,8587,8590],{},[105,8585,8586],{},"Minimum 24-hour gaps between emails",[105,8588,8589],{},"Maximum 7 emails per user",[105,8591,8592],{},"Disallowed enrolment of users (domain-based) upon CSM request",[19,8594,1048],{"id":1047},[102,8596,8597,8620,8653,8712,8742],{},[105,8598,8599,8602,8605,8606,8617,8619],{},[794,8600,8601],{},"Email Performance Varies by Context",[8603,8604],"br",{},"Our initial welcome email metrics showed:",[1070,8607,8608,8611,8614],{},[105,8609,8610],{},"Open Rate: 25.98%",[105,8612,8613],{},"Click Rate: 4.8%",[105,8615,8616],{},"Unsubscribe Rate: 0.53%",[8603,8618],{},"This baseline helped us understand where to focus our optimization efforts.",[105,8621,8622,8625,8627,8628,8630,8631,8639,8641,8642,8650,8652],{},[794,8623,8624],{},"Content Type Significantly Impacts Engagement",[8603,8626],{},"Different types of content showed varying levels of engagement:",[8603,8629],{},"Product Education Emails:",[1070,8632,8633,8636],{},[105,8634,8635],{},"\"Understanding your Dashboards\": 29.48% open rate",[105,8637,8638],{},"\"How to use purchased bundles\": 50% open rate",[8603,8640],{},"Feature Announcement Emails:",[1070,8643,8644,8647],{},[105,8645,8646],{},"\"Marketplace templates\": 30.67% open rate",[105,8648,8649],{},"\"Digitize your form\": 16.06% open rate",[8603,8651],{},"The data clearly showed that practical, how-to content consistently outperformed feature announcements.",[105,8654,8655,8658,8660,8661,8663,8664,8667,8678,8680,8681,8684,8695,8697,8698,8701],{},[794,8656,8657],{},"Key Optimization Learnings",[8603,8659],{},"Through A\u002FB testing and iteration, we identified several patterns:",[8603,8662],{},"a) ",[794,8665,8666],{},"Timing Matters",[1070,8668,8669,8672,8675],{},[105,8670,8671],{},"Emails sent after specific user actions (like bundle purchases) saw higher engagement",[105,8673,8674],{},"\"How to use purchased bundles\" achieved 50% open rate and 8.82% click rate",[105,8676,8677],{},"Immediate post-purchase timing proved more effective than delayed sends",[8603,8679],{},"b) ",[794,8682,8683],{},"Subject Line Impact",[1070,8685,8686,8689,8692],{},[105,8687,8688],{},"Direct, action-oriented subjects performed better",[105,8690,8691],{},"Including the word \"Dashboard\" improved open rates by ~10% (29.48% vs baseline)",[105,8693,8694],{},"Variations of \"How to create an Organization\" showed consistent ~25% open rates",[8603,8696],{},"c) ",[794,8699,8700],{},"Content Strategy",[1070,8702,8703,8706,8709],{},[105,8704,8705],{},"Task completion emails (\"Complete your form\") saw highest open rates (55.17%)",[105,8707,8708],{},"Technical guidance emails maintained steady ~18% engagement rates",[105,8710,8711],{},"Product education content needed to be highly specific to maintain engagement",[105,8713,8714,8717,8719,8720,8731,8733,8734],{},[794,8715,8716],{},"Unsubscribe Rate Patterns",[8603,8718],{},"We tracked unsubscribe rates carefully:",[1070,8721,8722,8725,8728],{},[105,8723,8724],{},"Early emails: 0.53% unsubscribe rate",[105,8726,8727],{},"Mid-flow emails: 1.5-3% unsubscribe rate",[105,8729,8730],{},"Later emails: 3-10% unsubscribe rate",[8603,8732],{},"This led to two key improvements:",[1070,8735,8736,8739],{},[105,8737,8738],{},"Implemented better user segmentation to reduce irrelevant sends",[105,8740,8741],{},"Added clearer expectations about email frequency in welcome message",[105,8743,8744,8747,8749,8750,8761,8763,8764],{},[794,8745,8746],{},"Skip Rate Insights",[8603,8748],{},"The skip rate data proved particularly valuable:",[1070,8751,8752,8755,8758],{},[105,8753,8754],{},"Early emails: \u003C 1% skip rate",[105,8756,8757],{},"Technical guides: 2-3% skip rate",[105,8759,8760],{},"Later workflow emails: 13-18% skip rate",[8603,8762],{},"This helped us:",[1070,8765,8766,8769,8772],{},[105,8767,8768],{},"Optimize email sequence ordering",[105,8770,8771],{},"Identify when users were ready to graduate from the nurture flow",[105,8773,8774],{},"Better time our CSM intervention points",[19,8776,8778],{"id":8777},"implementation-improvements","Implementation Improvements",[15,8780,8781],{},"Based on these learnings, I've made several tactical changes:",[102,8783,8784,8800,8816],{},[105,8785,8786,8789],{},[794,8787,8788],{},"Content",[1070,8790,8791,8794,8797],{},[105,8792,8793],{},"Shortened email copy by 30%",[105,8795,8796],{},"Added clear, single-action CTAs",[105,8798,8799],{},"Included specific use-case examples",[105,8801,8802,8805],{},[794,8803,8804],{},"Timing",[1070,8806,8807,8810,8813],{},[105,8808,8809],{},"Reduced frequency for users with high engagement",[105,8811,8812],{},"Added action-based triggers for key feature announcements",[105,8814,8815],{},"Implemented smart delays based on user timezone",[105,8817,8818,8821],{},[794,8819,8820],{},"Segmentation",[1070,8822,8823,8826,8829],{},[105,8824,8825],{},"Created separate flows for marketplace vs. DIY users",[105,8827,8828],{},"Adjusted content based on user's interaction history",[105,8830,8831],{},"Implemented engagement-based branching logic",[19,8833,8835],{"id":8834},"whats-possibly-next","What's (possibly 🤪) Next?",[102,8837,8838,8856,8871],{},[105,8839,8840,8843,8851,8853],{},[794,8841,8842],{},"Personalization",[1070,8844,8845,8848],{},[105,8846,8847],{},"Industry-specific content paths",[105,8849,8850],{},"Role-based recommendations",[8603,8852],{},[83,8854,8855],{},"(both only possible upon bettering our in-app onboarding + direct integration)",[105,8857,8858,8860],{},[794,8859,8788],{},[1070,8861,8862,8865,8868],{},[105,8863,8864],{},"Systematic subject line testing (e.g. \"How to create an Organization\" vs \"Creating an Organization is easy!\")",[105,8866,8867],{},"Send time optimization (delay vs trigger based)",[105,8869,8870],{},"Content format experimentation (e.g. images, videos, etc.)",[105,8872,8873,8876],{},[794,8874,8875],{},"Analytics",[1070,8877,8878,8881,8884],{},[105,8879,8880],{},"Deeper cohort analysis (limited by Hubspot's capabilities)",[105,8882,8883],{},"Engagement patterns analysis",[105,8885,8886],{},"Churn prediction integration (may be overkill for now as we're still learning)",[15,8888,8889],{},"The most valuable insight from this project is that email engagement isn't just: open rates \u002F click rates in isolation - it's about sending the right content to the right user at the right time so that the user derives value from the product. Our most successful emails weren't necessarily those with the highest open rates, but those that led to meaningful product engagement and user activation. 🧀",[1154,8891],{},[15,8893,8894],{},[83,8895,8896],{},"Note: All metrics are from actual production data from Q1 2025. While some metrics may seem modest compared to industry benchmarks, they represent real-world performance in the B2B SaaS space where engagement patterns differ significantly from B2C or general marketing emails.",{"title":48,"searchDepth":49,"depth":49,"links":8898},[8899,8900,8901,8906,8907,8908],{"id":77,"depth":49,"text":78},{"id":125,"depth":49,"text":126},{"id":1239,"depth":49,"text":1240,"children":8902},[8903,8904,8905],{"id":1243,"depth":216,"text":1244},{"id":1272,"depth":216,"text":1273},{"id":4636,"depth":216,"text":4637},{"id":1047,"depth":49,"text":1048},{"id":8777,"depth":49,"text":8778},{"id":8834,"depth":49,"text":8835},"2024-02-27","Engineered a dynamic, behavior-driven email nurture flow that adapts to user actions and engagement patterns, leading to significant improvements in user activation and engagement metrics.",{},"\u002Fwork\u002Fhubspot-nurture-flow-automation",{"title":8431,"description":8910},"work\u002Fhubspot-nurture-flow-automation",[8916,1189,8917,8918,8919],"HubSpot","CRM","Email Marketing","Workflow Design","fnV3vZ1ycfMutwnD_frRKcpJZIr0UPnv_7g8FkCs5rk",[8922,9793],{"id":8923,"title":8924,"author":1180,"body":8925,"date":9780,"description":9781,"extension":55,"meta":9782,"navigation":56,"path":9784,"seo":9785,"stem":9786,"tags":9787,"__hash__":9792},"writing\u002Fwriting\u002Freactflow-workflow-visualization.md","ReactFlow: Building Intuitive Workflow Visualizations for Non-Technical Users",{"type":8,"value":8926,"toc":9767},[8927,8930,8934,8937,8944,8948,8951,8960,8964,8968,8971,9193,9196,9207,9211,9214,9425,9428,9432,9435,9443,9488,9496,9600,9608,9672,9676,9679,9698,9702,9705,9719,9723,9754,9758,9761,9764],[11,8928,8924],{"id":8929},"reactflow-building-intuitive-workflow-visualizations-for-non-technical-users",[19,8931,8933],{"id":8932},"why-im-writing-this","Why I'm Writing This",[15,8935,8936],{},"Recently, I faced an interesting challenge while building our marketing website at Capptions. We needed to showcase our Workflow Builder functionality to QHSE (Quality, Health, Safety, and Environment) managers - a user group that spans the spectrum from tech-savvy to non-technical backgrounds. The goal was to make complex workflow concepts accessible and visually appealing - so users understand prior having to sign up.",[15,8938,8939,8940,8943],{},"After exploring other options ",[83,8941,8942],{},"(plain Tailwind, motion.dev)",", I landed on ReactFlow, and the journey taught me valuable lessons about technical documentation, user experience, and the art of simplifying complex concepts.",[19,8945,8947],{"id":8946},"key-insight-visualization-explanation","Key Insight: Visualization > Explanation",[15,8949,8950],{},"The main \"aha\" moment came when I realized that trying to explain workflow builders through text and static images wasn't cutting it. Our users needed something interactive and familiar - something that reflected the actual tool they'd be using.",[15,8952,8953],{},[7257,8954],{"src":8955,"alt":8956,"className":8957},"\u002Fimages\u002Fcontent\u002Freact-flow-workflow-visualisation.webp","ReactFlow Workflow Visualization",[8958,8959,8494],"w-3\u002F4","mx-auto",[19,8961,8963],{"id":8962},"deep-dive","Deep Dive",[148,8965,8967],{"id":8966},"setting-up-custom-nodes","Setting Up Custom Nodes",[15,8969,8970],{},"The first challenge was creating custom nodes that felt native to our application. Here's how I approached it:",[156,8972,8974],{"className":158,"code":8973,"language":160,"meta":48,"style":48},"type CustomNodeData = {\n  label: React.ReactNode;\n};\n\nconst CustomNode = ({ data }: { data: CustomNodeData }) => (\n  \u003Cdiv className=\"relative p-2 bg-white rounded-md border-2 border-gray-300\">\n    \u003CHandle\n      type=\"target\"\n      position={Position.Top}\n      className=\"!w-3 !h-3 !bg-primary !border-1 !border-white ring-1 ring-offset-2 ring-gray-300\"\n    \u002F>\n    {data.label}\n    \u003CHandle\n      type=\"source\"\n      position={Position.Bottom}\n      className=\"!w-3 !h-3 !bg-primary !border-1 !border-white ring-1 ring-offset-2 ring-gray-300\"\n    \u002F>\n  \u003C\u002Fdiv>\n);\n",[162,8975,8976,8988,9003,9007,9011,9043,9062,9069,9084,9098,9112,9117,9130,9136,9149,9162,9174,9178,9187],{"__ignoreMap":48},[165,8977,8978,8981,8984,8986],{"class":167,"line":168},[165,8979,8980],{"class":171},"type",[165,8982,8983],{"class":1293}," CustomNodeData",[165,8985,180],{"class":179},[165,8987,1297],{"class":194},[165,8989,8990,8992,8994,8996,8998,9001],{"class":167,"line":49},[165,8991,1438],{"class":1321},[165,8993,451],{"class":179},[165,8995,7986],{"class":1293},[165,8997,46],{"class":194},[165,8999,9000],{"class":1293},"ReactNode",[165,9002,195],{"class":194},[165,9004,9005],{"class":167,"line":216},[165,9006,1515],{"class":194},[165,9008,9009],{"class":167,"line":237},[165,9010,240],{"emptyLinePlaceholder":56},[165,9012,9013,9015,9018,9020,9022,9025,9027,9029,9031,9033,9035,9037,9039,9041],{"class":167,"line":243},[165,9014,172],{"class":171},[165,9016,9017],{"class":5661}," CustomNode",[165,9019,180],{"class":179},[165,9021,7486],{"class":194},[165,9023,9024],{"class":536}," data",[165,9026,4499],{"class":194},[165,9028,451],{"class":179},[165,9030,4493],{"class":194},[165,9032,9024],{"class":1321},[165,9034,451],{"class":179},[165,9036,8983],{"class":1293},[165,9038,7502],{"class":194},[165,9040,761],{"class":171},[165,9042,7768],{"class":256},[165,9044,9045,9048,9051,9053,9055,9058,9060],{"class":167,"line":249},[165,9046,9047],{"class":179},"  \u003C",[165,9049,9050],{"class":256},"div className",[165,9052,266],{"class":179},[165,9054,191],{"class":183},[165,9056,9057],{"class":187},"relative p-2 bg-white rounded-md border-2 border-gray-300",[165,9059,191],{"class":183},[165,9061,7789],{"class":179},[165,9063,9064,9066],{"class":167,"line":295},[165,9065,7773],{"class":179},[165,9067,9068],{"class":536},"Handle\n",[165,9070,9071,9074,9076,9078,9081],{"class":167,"line":348},[165,9072,9073],{"class":256},"      type",[165,9075,266],{"class":179},[165,9077,191],{"class":183},[165,9079,9080],{"class":187},"target",[165,9082,9083],{"class":183},"\"\n",[165,9085,9086,9089,9091,9093,9096],{"class":167,"line":353},[165,9087,9088],{"class":256},"      position",[165,9090,266],{"class":179},[165,9092,1611],{"class":194},[165,9094,9095],{"class":256},"Position.Top",[165,9097,784],{"class":194},[165,9099,9100,9103,9105,9107,9110],{"class":167,"line":373},[165,9101,9102],{"class":256},"      className",[165,9104,266],{"class":179},[165,9106,191],{"class":183},[165,9108,9109],{"class":187},"!w-3 !h-3 !bg-primary !border-1 !border-white ring-1 ring-offset-2 ring-gray-300",[165,9111,9083],{"class":183},[165,9113,9114],{"class":167,"line":387},[165,9115,9116],{"class":179},"    \u002F>\n",[165,9118,9119,9121,9123,9125,9128],{"class":167,"line":401},[165,9120,4271],{"class":194},[165,9122,458],{"class":536},[165,9124,46],{"class":256},[165,9126,9127],{"class":536},"label",[165,9129,784],{"class":194},[165,9131,9132,9134],{"class":167,"line":413},[165,9133,7773],{"class":179},[165,9135,9068],{"class":536},[165,9137,9138,9140,9142,9144,9147],{"class":167,"line":421},[165,9139,9073],{"class":256},[165,9141,266],{"class":179},[165,9143,191],{"class":183},[165,9145,9146],{"class":187},"source",[165,9148,9083],{"class":183},[165,9150,9151,9153,9155,9157,9160],{"class":167,"line":426},[165,9152,9088],{"class":256},[165,9154,266],{"class":179},[165,9156,1611],{"class":194},[165,9158,9159],{"class":256},"Position.Bottom",[165,9161,784],{"class":194},[165,9163,9164,9166,9168,9170,9172],{"class":167,"line":445},[165,9165,9102],{"class":256},[165,9167,266],{"class":179},[165,9169,191],{"class":183},[165,9171,9109],{"class":187},[165,9173,9083],{"class":183},[165,9175,9176],{"class":167,"line":472},[165,9177,9116],{"class":179},[165,9179,9180,9183,9185],{"class":167,"line":489},[165,9181,9182],{"class":179},"  \u003C\u002F",[165,9184,7776],{"class":256},[165,9186,7789],{"class":179},[165,9188,9189,9191],{"class":167,"line":499},[165,9190,343],{"class":256},[165,9192,195],{"class":194},[15,9194,9195],{},"I designed the nodes with a few key principles in mind:",[1070,9197,9198,9201,9204],{},[105,9199,9200],{},"Clean, minimal styling that matches our UI",[105,9202,9203],{},"Clear connection points (handles) for visual flow",[105,9205,9206],{},"Flexible content rendering through React nodes",[148,9208,9210],{"id":9209},"state-management-and-flow-control","State Management and Flow Control",[15,9212,9213],{},"The core flow management is surprisingly straightforward with ReactFlow's hooks:",[156,9215,9217],{"className":158,"code":9216,"language":160,"meta":48,"style":48},"const [nodes, setNodes] = useState\u003CNode[]>(initialNodes);\nconst [edges, setEdges] = useState(initialEdges);\n\nconst onNodesChange = useCallback(\n  (changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)),\n  [],\n);\n\nconst onEdgesChange = useCallback(\n  (changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),\n  [],\n);\n",[162,9218,9219,9253,9278,9282,9296,9341,9348,9354,9358,9371,9413,9419],{"__ignoreMap":48},[165,9220,9221,9223,9225,9228,9230,9233,9235,9237,9239,9241,9244,9246,9248,9251],{"class":167,"line":168},[165,9222,172],{"class":171},[165,9224,5839],{"class":194},[165,9226,9227],{"class":175},"nodes",[165,9229,682],{"class":194},[165,9231,9232],{"class":175}," setNodes",[165,9234,942],{"class":194},[165,9236,180],{"class":179},[165,9238,7527],{"class":303},[165,9240,276],{"class":194},[165,9242,9243],{"class":1293},"Node",[165,9245,2584],{"class":256},[165,9247,3432],{"class":194},[165,9249,9250],{"class":256},"(initialNodes)",[165,9252,195],{"class":194},[165,9254,9255,9257,9259,9262,9264,9267,9269,9271,9273,9276],{"class":167,"line":49},[165,9256,172],{"class":171},[165,9258,5839],{"class":194},[165,9260,9261],{"class":175},"edges",[165,9263,682],{"class":194},[165,9265,9266],{"class":175}," setEdges",[165,9268,942],{"class":194},[165,9270,180],{"class":179},[165,9272,7527],{"class":303},[165,9274,9275],{"class":256},"(initialEdges)",[165,9277,195],{"class":194},[165,9279,9280],{"class":167,"line":216},[165,9281,240],{"emptyLinePlaceholder":56},[165,9283,9284,9286,9289,9291,9294],{"class":167,"line":237},[165,9285,172],{"class":171},[165,9287,9288],{"class":175}," onNodesChange",[165,9290,180],{"class":179},[165,9292,9293],{"class":303}," useCallback",[165,9295,370],{"class":256},[165,9297,9298,9301,9304,9306,9309,9311,9313,9315,9317,9319,9321,9324,9326,9328,9331,9334,9336,9339],{"class":167,"line":243},[165,9299,9300],{"class":194},"  (",[165,9302,9303],{"class":536},"changes",[165,9305,451],{"class":179},[165,9307,9308],{"class":1293}," NodeChange",[165,9310,2584],{"class":256},[165,9312,343],{"class":194},[165,9314,761],{"class":171},[165,9316,9232],{"class":303},[165,9318,308],{"class":256},[165,9320,308],{"class":194},[165,9322,9323],{"class":536},"nds",[165,9325,343],{"class":194},[165,9327,761],{"class":171},[165,9329,9330],{"class":303}," applyNodeChanges",[165,9332,9333],{"class":256},"(changes",[165,9335,682],{"class":194},[165,9337,9338],{"class":256}," nds))",[165,9340,384],{"class":194},[165,9342,9343,9346],{"class":167,"line":249},[165,9344,9345],{"class":256},"  []",[165,9347,384],{"class":194},[165,9349,9350,9352],{"class":167,"line":295},[165,9351,343],{"class":256},[165,9353,195],{"class":194},[165,9355,9356],{"class":167,"line":348},[165,9357,240],{"emptyLinePlaceholder":56},[165,9359,9360,9362,9365,9367,9369],{"class":167,"line":353},[165,9361,172],{"class":171},[165,9363,9364],{"class":175}," onEdgesChange",[165,9366,180],{"class":179},[165,9368,9293],{"class":303},[165,9370,370],{"class":256},[165,9372,9373,9375,9377,9379,9382,9384,9386,9388,9390,9392,9394,9397,9399,9401,9404,9406,9408,9411],{"class":167,"line":373},[165,9374,9300],{"class":194},[165,9376,9303],{"class":536},[165,9378,451],{"class":179},[165,9380,9381],{"class":1293}," EdgeChange",[165,9383,2584],{"class":256},[165,9385,343],{"class":194},[165,9387,761],{"class":171},[165,9389,9266],{"class":303},[165,9391,308],{"class":256},[165,9393,308],{"class":194},[165,9395,9396],{"class":536},"eds",[165,9398,343],{"class":194},[165,9400,761],{"class":171},[165,9402,9403],{"class":303}," applyEdgeChanges",[165,9405,9333],{"class":256},[165,9407,682],{"class":194},[165,9409,9410],{"class":256}," eds))",[165,9412,384],{"class":194},[165,9414,9415,9417],{"class":167,"line":387},[165,9416,9345],{"class":256},[165,9418,384],{"class":194},[165,9420,9421,9423],{"class":167,"line":401},[165,9422,343],{"class":256},[165,9424,195],{"class":194},[15,9426,9427],{},"What I love about this approach is how ReactFlow handles the complex state management internally while exposing a clean API for customization.",[148,9429,9431],{"id":9430},"making-it-non-technical-user-friendly","Making It Non-Technical User Friendly",[15,9433,9434],{},"Here's where things got interesting. For our QHSC managers, I implemented several UX decisions:",[102,9436,9437],{},[105,9438,9439,9442],{},[794,9440,9441],{},"Locked Navigation",": Prevented accidental canvas dragging",[156,9444,9446],{"className":158,"code":9445,"language":160,"meta":48,"style":48},"\u003CReactFlow panOnDrag={false} draggable={false} preventScrolling={false} \u002F>\n",[162,9447,9448],{"__ignoreMap":48},[165,9449,9450,9452,9455,9457,9459,9461,9463,9466,9468,9470,9472,9474,9477,9479,9481,9483,9485],{"class":167,"line":168},[165,9451,276],{"class":179},[165,9453,9454],{"class":256},"ReactFlow panOnDrag",[165,9456,266],{"class":179},[165,9458,1611],{"class":194},[165,9460,7532],{"class":256},[165,9462,329],{"class":194},[165,9464,9465],{"class":256}," draggable",[165,9467,266],{"class":179},[165,9469,1611],{"class":194},[165,9471,7532],{"class":256},[165,9473,329],{"class":194},[165,9475,9476],{"class":256}," preventScrolling",[165,9478,266],{"class":179},[165,9480,1611],{"class":194},[165,9482,7532],{"class":256},[165,9484,329],{"class":194},[165,9486,9487],{"class":179}," \u002F>\n",[102,9489,9490],{"start":49},[105,9491,9492,9495],{},[794,9493,9494],{},"Visual Feedback",": Added animated edges to show flow direction",[156,9497,9499],{"className":158,"code":9498,"language":160,"meta":48,"style":48},"const initialEdges: Edge[] = [\n  {\n    id: \"start-to-inspection\",\n    source: \"start\",\n    target: \"inspection\",\n    animated: true, \u002F\u002F This small detail makes a big difference\n  },\n  \u002F\u002F ...\n];\n",[162,9500,9501,9519,9523,9538,9554,9570,9585,9589,9594],{"__ignoreMap":48},[165,9502,9503,9505,9508,9510,9513,9515,9517],{"class":167,"line":168},[165,9504,172],{"class":171},[165,9506,9507],{"class":175}," initialEdges",[165,9509,451],{"class":179},[165,9511,9512],{"class":1293}," Edge",[165,9514,2845],{"class":256},[165,9516,266],{"class":179},[165,9518,1899],{"class":256},[165,9520,9521],{"class":167,"line":49},[165,9522,2854],{"class":194},[165,9524,9525,9527,9529,9531,9534,9536],{"class":167,"line":216},[165,9526,1322],{"class":307},[165,9528,451],{"class":194},[165,9530,184],{"class":183},[165,9532,9533],{"class":187},"start-to-inspection",[165,9535,191],{"class":183},[165,9537,384],{"class":194},[165,9539,9540,9543,9545,9547,9550,9552],{"class":167,"line":237},[165,9541,9542],{"class":307},"    source",[165,9544,451],{"class":194},[165,9546,184],{"class":183},[165,9548,9549],{"class":187},"start",[165,9551,191],{"class":183},[165,9553,384],{"class":194},[165,9555,9556,9559,9561,9563,9566,9568],{"class":167,"line":243},[165,9557,9558],{"class":307},"    target",[165,9560,451],{"class":194},[165,9562,184],{"class":183},[165,9564,9565],{"class":187},"inspection",[165,9567,191],{"class":183},[165,9569,384],{"class":194},[165,9571,9572,9575,9577,9580,9582],{"class":167,"line":249},[165,9573,9574],{"class":307},"    animated",[165,9576,451],{"class":194},[165,9578,9579],{"class":5797}," true",[165,9581,682],{"class":194},[165,9583,9584],{"class":233}," \u002F\u002F This small detail makes a big difference\n",[165,9586,9587],{"class":167,"line":295},[165,9588,1816],{"class":194},[165,9590,9591],{"class":167,"line":348},[165,9592,9593],{"class":233},"  \u002F\u002F ...\n",[165,9595,9596,9598],{"class":167,"line":353},[165,9597,942],{"class":256},[165,9599,195],{"class":194},[102,9601,9602],{"start":216},[105,9603,9604,9607],{},[794,9605,9606],{},"Intuitive Icons",": Used familiar icons for different node types",[156,9609,9611],{"className":158,"code":9610,"language":160,"meta":48,"style":48},"\u003Cdiv className=\"flex gap-2 items-center font-medium text-indigo-900\">\n  \u003CClipboardCheck className=\"w-4 h-4\" \u002F>\n  \u003Cspan>Inspection\u003C\u002Fspan>\n\u003C\u002Fdiv>\n",[162,9612,9613,9630,9648,9664],{"__ignoreMap":48},[165,9614,9615,9617,9619,9621,9623,9626,9628],{"class":167,"line":168},[165,9616,276],{"class":179},[165,9618,9050],{"class":256},[165,9620,266],{"class":179},[165,9622,191],{"class":183},[165,9624,9625],{"class":187},"flex gap-2 items-center font-medium text-indigo-900",[165,9627,191],{"class":183},[165,9629,7789],{"class":179},[165,9631,9632,9634,9637,9639,9641,9644,9646],{"class":167,"line":49},[165,9633,9047],{"class":179},[165,9635,9636],{"class":256},"ClipboardCheck className",[165,9638,266],{"class":179},[165,9640,191],{"class":183},[165,9642,9643],{"class":187},"w-4 h-4",[165,9645,191],{"class":183},[165,9647,9487],{"class":179},[165,9649,9650,9652,9654,9657,9660,9662],{"class":167,"line":216},[165,9651,9047],{"class":256},[165,9653,165],{"class":1293},[165,9655,9656],{"class":256},">Inspection",[165,9658,9659],{"class":179},"\u003C\u002F",[165,9661,165],{"class":256},[165,9663,7789],{"class":179},[165,9665,9666,9668,9670],{"class":167,"line":237},[165,9667,9659],{"class":179},[165,9669,7776],{"class":256},[165,9671,7789],{"class":179},[19,9673,9675],{"id":9674},"what-id-do-differently","What I'd Do Differently",[15,9677,9678],{},"Looking back, there are a few things I'd approach differently:",[102,9680,9681,9687,9693],{},[105,9682,9683,9686],{},[794,9684,9685],{},"Start with Mobile-First",": While ReactFlow works great on desktop, I should have considered mobile interactions earlier in the development process.",[105,9688,9689,9692],{},[794,9690,9691],{},"More Interactive Elements",": I could have added more interactive elements to demonstrate the actual workflow building process, not just the final result.",[105,9694,9695,9697],{},[794,9696,2392],{},": For larger workflows, I'd implement virtualization earlier in the development cycle.",[19,9699,9701],{"id":9700},"the-documentation-inspiration","The Documentation Inspiration",[15,9703,9704],{},"One unexpected outcome was how much I learned from ReactFlow's documentation. It's a masterclass in technical writing - clear, well-structured, and practical. It inspired me to improve our own documentation with:",[1070,9706,9707,9710,9713,9716],{},[105,9708,9709],{},"Interactive examples",[105,9711,9712],{},"Clear, concise code snippets",[105,9714,9715],{},"Progressive disclosure of complexity",[105,9717,9718],{},"Practical use cases",[19,9720,9722],{"id":9721},"resources","Resources",[1070,9724,9725,9732,9739,9747],{},[105,9726,9727],{},[34,9728,9731],{"href":9729,"rel":9730},"https:\u002F\u002Freactflow.dev\u002Fdocs\u002Fintroduction\u002F",[38],"ReactFlow Documentation",[105,9733,9734],{},[34,9735,9738],{"href":9736,"rel":9737},"https:\u002F\u002Freactflow.dev\u002Fdocs\u002Fexamples\u002Foverview\u002F",[38],"React Flow Examples",[105,9740,9741,9746],{},[34,9742,9745],{"href":9743,"rel":9744},"https:\u002F\u002Flucide.dev\u002F",[38],"Lucide Icons"," - For the workflow icons",[105,9748,9749],{},[34,9750,9753],{"href":9751,"rel":9752},"https:\u002F\u002Fwww.patterns.dev\u002Freact",[38],"TypeScript React Patterns",[19,9755,9757],{"id":9756},"final-thoughts","Final Thoughts",[15,9759,9760],{},"Building this workflow visualization reminded me that sometimes the best technical solution isn't about showing off complex features - it's about making complex things feel simple. RF definitely helped me bridge the gap between technical capability and user understanding, which is exactly what we needed for our marketing site.",[15,9762,9763],{},"The next time you're faced with explaining complex technical concepts to non-technical users, consider whether a visual, interactive approach might be more effective than traditional documentation or static diagrams.",[1162,9765,9766],{},"html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sucvu, html code.shiki .sucvu{--shiki-light:#E53935;--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sfCm-, html code.shiki .sfCm-{--shiki-light:#90A4AE;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}",{"title":48,"searchDepth":49,"depth":49,"links":9768},[9769,9770,9771,9776,9777,9778,9779],{"id":8932,"depth":49,"text":8933},{"id":8946,"depth":49,"text":8947},{"id":8962,"depth":49,"text":8963,"children":9772},[9773,9774,9775],{"id":8966,"depth":216,"text":8967},{"id":9209,"depth":216,"text":9210},{"id":9430,"depth":216,"text":9431},{"id":9674,"depth":49,"text":9675},{"id":9700,"depth":49,"text":9701},{"id":9721,"depth":49,"text":9722},{"id":9756,"depth":49,"text":9757},"2025-06-23","Using ReactFlow to build intuitive workflow visualizations for non-technical users, with insights on creating custom nodes and handling complex state management.",{"slug":9783},"reactflow-workflow-visualization","\u002Fwriting\u002Freactflow-workflow-visualization",{"title":8924,"description":9781},"writing\u002Freactflow-workflow-visualization",[9788,9789,64,9790,9791],"React","ReactFlow","UI\u002FUX","Workflow Visualization","lVPty6QJp8WNFtvnnl40UxeZKhVzJHO7UAHJd0wV-sU",{"id":9794,"title":9795,"author":1180,"body":9796,"date":10949,"description":10950,"extension":55,"meta":10951,"navigation":56,"path":10953,"seo":10954,"stem":10955,"tags":10956,"__hash__":10961},"writing\u002Fwriting\u002Fbrowser-extension-messaging.md","Understanding Browser Extension Messaging",{"type":8,"value":9797,"toc":10939},[9798,9801,9803,9806,9809,9813,9820,9826,9830,9834,9837,9926,9930,9944,10180,10184,10195,10353,10357,10368,10489,10493,10504,10508,10515,10632,10639,10774,10781,10885,10887,10907,10910,10912,10936],[11,9799,9795],{"id":9800},"understanding-browser-extension-messaging",[19,9802,8933],{"id":8932},[15,9804,9805],{},"While building a Chrome extension for automated documentation generation (a story for another day), I found myself drowning in a sea of browser extension messaging concepts. Background scripts, content scripts, popup windows, offscreen documents – each piece seemed simple in isolation, but orchestrating communication between them felt like conducting an orchestra where every musician was in a different room.",[15,9807,9808],{},"After many late nights of debugging and several \"aha\" moments, I've developed a mental model that I wish I had when starting.",[19,9810,9812],{"id":9811},"the-key-mental-model","The Key Mental Model",[15,9814,9815,9816,9819],{},"Think of a browser extension as a distributed system running in a single browser. Each component (background worker, popup, content script) is like a microservice with its own lifecycle and constraints. The key to mastery? Understanding not just how they communicate, but ",[83,9817,9818],{},"why"," they're separated in the first place.",[15,9821,9822],{},[7257,9823],{"alt":9824,"src":9825},"Extension Components Diagram","\u002Fimages\u002Fcontent\u002Fbrowser-architecture.webp",[19,9827,9829],{"id":9828},"deep-dive-into-extension-messaging","Deep Dive into Extension Messaging",[148,9831,9833],{"id":9832},"_1-the-players-in-our-distributed-system","1. The Players in Our Distributed System",[15,9835,9836],{},"Let's break down each component and its role:",[156,9838,9840],{"className":158,"code":9839,"language":160,"meta":48,"style":48},"\u002F\u002F Example message type definition\ninterface Message {\n  target: \"background\" | \"content-script\" | \"popup\" | \"offscreen\";\n  action: string;\n  data?: unknown;\n}\n",[162,9841,9842,9847,9856,9899,9910,9922],{"__ignoreMap":48},[165,9843,9844],{"class":167,"line":168},[165,9845,9846],{"class":233},"\u002F\u002F Example message type definition\n",[165,9848,9849,9851,9854],{"class":167,"line":49},[165,9850,1290],{"class":171},[165,9852,9853],{"class":1293}," Message",[165,9855,1297],{"class":194},[165,9857,9858,9861,9863,9865,9868,9870,9872,9874,9877,9879,9881,9883,9886,9888,9890,9892,9895,9897],{"class":167,"line":216},[165,9859,9860],{"class":1321},"  target",[165,9862,451],{"class":179},[165,9864,184],{"class":183},[165,9866,9867],{"class":187},"background",[165,9869,191],{"class":183},[165,9871,2078],{"class":179},[165,9873,184],{"class":183},[165,9875,9876],{"class":187},"content-script",[165,9878,191],{"class":183},[165,9880,2078],{"class":179},[165,9882,184],{"class":183},[165,9884,9885],{"class":187},"popup",[165,9887,191],{"class":183},[165,9889,2078],{"class":179},[165,9891,184],{"class":183},[165,9893,9894],{"class":187},"offscreen",[165,9896,191],{"class":183},[165,9898,195],{"class":194},[165,9900,9901,9904,9906,9908],{"class":167,"line":237},[165,9902,9903],{"class":1321},"  action",[165,9905,451],{"class":179},[165,9907,1310],{"class":748},[165,9909,195],{"class":194},[165,9911,9912,9915,9917,9920],{"class":167,"line":243},[165,9913,9914],{"class":1321},"  data",[165,9916,2104],{"class":179},[165,9918,9919],{"class":748}," unknown",[165,9921,195],{"class":194},[165,9923,9924],{"class":167,"line":249},[165,9925,784],{"class":194},[2770,9927,9929],{"id":9928},"background-service-worker","Background Service Worker",[1070,9931,9932,9935,9938,9941],{},[105,9933,9934],{},"The orchestrator",[105,9936,9937],{},"Always running (but can be inactive)",[105,9939,9940],{},"Can't access DOM",[105,9942,9943],{},"Handles long-running tasks",[156,9945,9947],{"className":158,"code":9946,"language":160,"meta":48,"style":48},"\u002F\u002F From my actual implementation\nexport default defineBackground(() => {\n  browser.runtime.onMessage.addListener((message, sender, sendResponse) => {\n    if (message.target !== \"background\") return;\n\n    const handleMessage = async () => {\n      switch (message.action) {\n        case \"start\":\n          \u002F\u002F Handle start action\n          break;\n        case \"stop\":\n          \u002F\u002F Handle stop action\n          break;\n      }\n    };\n\n    handleMessage();\n    return true; \u002F\u002F Important for async responses!\n  });\n});\n",[162,9948,9949,9954,9972,10014,10042,10046,10063,10081,10095,10100,10107,10120,10125,10131,10136,10140,10144,10153,10164,10172],{"__ignoreMap":48},[165,9950,9951],{"class":167,"line":168},[165,9952,9953],{"class":233},"\u002F\u002F From my actual implementation\n",[165,9955,9956,9958,9961,9964,9966,9968,9970],{"class":167,"line":49},[165,9957,7475],{"class":252},[165,9959,9960],{"class":252}," default",[165,9962,9963],{"class":303}," defineBackground",[165,9965,308],{"class":256},[165,9967,914],{"class":194},[165,9969,761],{"class":171},[165,9971,1297],{"class":194},[165,9973,9974,9977,9979,9982,9984,9987,9989,9992,9994,9996,9998,10000,10003,10005,10008,10010,10012],{"class":167,"line":216},[165,9975,9976],{"class":256},"  browser",[165,9978,46],{"class":194},[165,9980,9981],{"class":256},"runtime",[165,9983,46],{"class":194},[165,9985,9986],{"class":256},"onMessage",[165,9988,46],{"class":194},[165,9990,9991],{"class":303},"addListener",[165,9993,308],{"class":307},[165,9995,308],{"class":194},[165,9997,3579],{"class":536},[165,9999,682],{"class":194},[165,10001,10002],{"class":536}," sender",[165,10004,682],{"class":194},[165,10006,10007],{"class":536}," sendResponse",[165,10009,343],{"class":194},[165,10011,761],{"class":171},[165,10013,1297],{"class":194},[165,10015,10016,10018,10020,10022,10024,10026,10029,10031,10033,10035,10037,10040],{"class":167,"line":237},[165,10017,6765],{"class":252},[165,10019,257],{"class":307},[165,10021,3579],{"class":256},[165,10023,46],{"class":194},[165,10025,9080],{"class":256},[165,10027,10028],{"class":179}," !==",[165,10030,184],{"class":183},[165,10032,9867],{"class":187},[165,10034,191],{"class":183},[165,10036,289],{"class":307},[165,10038,10039],{"class":252},"return",[165,10041,195],{"class":194},[165,10043,10044],{"class":167,"line":243},[165,10045,240],{"emptyLinePlaceholder":56},[165,10047,10048,10050,10053,10055,10057,10059,10061],{"class":167,"line":249},[165,10049,604],{"class":171},[165,10051,10052],{"class":5661}," handleMessage",[165,10054,180],{"class":179},[165,10056,5667],{"class":171},[165,10058,7731],{"class":194},[165,10060,761],{"class":171},[165,10062,1297],{"class":194},[165,10064,10065,10068,10070,10072,10074,10077,10079],{"class":167,"line":295},[165,10066,10067],{"class":252},"      switch",[165,10069,257],{"class":307},[165,10071,3579],{"class":256},[165,10073,46],{"class":194},[165,10075,10076],{"class":256},"action",[165,10078,289],{"class":307},[165,10080,292],{"class":194},[165,10082,10083,10086,10088,10090,10092],{"class":167,"line":348},[165,10084,10085],{"class":252},"        case",[165,10087,184],{"class":183},[165,10089,9549],{"class":187},[165,10091,191],{"class":183},[165,10093,10094],{"class":194},":\n",[165,10096,10097],{"class":167,"line":353},[165,10098,10099],{"class":233},"          \u002F\u002F Handle start action\n",[165,10101,10102,10105],{"class":167,"line":373},[165,10103,10104],{"class":252},"          break",[165,10106,195],{"class":194},[165,10108,10109,10111,10113,10116,10118],{"class":167,"line":387},[165,10110,10085],{"class":252},[165,10112,184],{"class":183},[165,10114,10115],{"class":187},"stop",[165,10117,191],{"class":183},[165,10119,10094],{"class":194},[165,10121,10122],{"class":167,"line":401},[165,10123,10124],{"class":233},"          \u002F\u002F Handle stop action\n",[165,10126,10127,10129],{"class":167,"line":413},[165,10128,10104],{"class":252},[165,10130,195],{"class":194},[165,10132,10133],{"class":167,"line":421},[165,10134,10135],{"class":194},"      }\n",[165,10137,10138],{"class":167,"line":426},[165,10139,2241],{"class":194},[165,10141,10142],{"class":167,"line":445},[165,10143,240],{"emptyLinePlaceholder":56},[165,10145,10146,10149,10151],{"class":167,"line":472},[165,10147,10148],{"class":303},"    handleMessage",[165,10150,914],{"class":307},[165,10152,195],{"class":194},[165,10154,10155,10157,10159,10161],{"class":167,"line":489},[165,10156,4360],{"class":252},[165,10158,9579],{"class":5797},[165,10160,230],{"class":194},[165,10162,10163],{"class":233}," \u002F\u002F Important for async responses!\n",[165,10165,10166,10168,10170],{"class":167,"line":499},[165,10167,492],{"class":194},[165,10169,343],{"class":307},[165,10171,195],{"class":194},[165,10173,10174,10176,10178],{"class":167,"line":504},[165,10175,329],{"class":194},[165,10177,343],{"class":256},[165,10179,195],{"class":194},[2770,10181,10183],{"id":10182},"content-scripts","Content Scripts",[1070,10185,10186,10189,10192],{},[105,10187,10188],{},"Your \"eyes and ears\" in the webpage",[105,10190,10191],{},"Can access DOM",[105,10193,10194],{},"Limited access to extension APIs",[156,10196,10198],{"className":158,"code":10197,"language":160,"meta":48,"style":48},"\u002F\u002F Content script message handling\nbrowser.runtime.onMessage.addListener(async (message) => {\n  if (message.target !== \"content-script\") return;\n\n  switch (message.action) {\n    case \"track-dom\":\n      startDomTracking();\n      break;\n    case \"stop-tracking\":\n      stopDomTracking();\n      break;\n  }\n});\n",[162,10199,10200,10205,10236,10262,10266,10283,10297,10306,10313,10326,10335,10341,10345],{"__ignoreMap":48},[165,10201,10202],{"class":167,"line":168},[165,10203,10204],{"class":233},"\u002F\u002F Content script message handling\n",[165,10206,10207,10210,10212,10214,10216,10218,10220,10222,10224,10226,10228,10230,10232,10234],{"class":167,"line":49},[165,10208,10209],{"class":256},"browser",[165,10211,46],{"class":194},[165,10213,9981],{"class":256},[165,10215,46],{"class":194},[165,10217,9986],{"class":256},[165,10219,46],{"class":194},[165,10221,9991],{"class":303},[165,10223,308],{"class":256},[165,10225,3380],{"class":171},[165,10227,257],{"class":194},[165,10229,3579],{"class":536},[165,10231,343],{"class":194},[165,10233,761],{"class":171},[165,10235,1297],{"class":194},[165,10237,10238,10240,10242,10244,10246,10248,10250,10252,10254,10256,10258,10260],{"class":167,"line":216},[165,10239,589],{"class":252},[165,10241,257],{"class":307},[165,10243,3579],{"class":256},[165,10245,46],{"class":194},[165,10247,9080],{"class":256},[165,10249,10028],{"class":179},[165,10251,184],{"class":183},[165,10253,9876],{"class":187},[165,10255,191],{"class":183},[165,10257,289],{"class":307},[165,10259,10039],{"class":252},[165,10261,195],{"class":194},[165,10263,10264],{"class":167,"line":237},[165,10265,240],{"emptyLinePlaceholder":56},[165,10267,10268,10271,10273,10275,10277,10279,10281],{"class":167,"line":243},[165,10269,10270],{"class":252},"  switch",[165,10272,257],{"class":307},[165,10274,3579],{"class":256},[165,10276,46],{"class":194},[165,10278,10076],{"class":256},[165,10280,289],{"class":307},[165,10282,292],{"class":194},[165,10284,10285,10288,10290,10293,10295],{"class":167,"line":249},[165,10286,10287],{"class":252},"    case",[165,10289,184],{"class":183},[165,10291,10292],{"class":187},"track-dom",[165,10294,191],{"class":183},[165,10296,10094],{"class":194},[165,10298,10299,10302,10304],{"class":167,"line":295},[165,10300,10301],{"class":303},"      startDomTracking",[165,10303,914],{"class":307},[165,10305,195],{"class":194},[165,10307,10308,10311],{"class":167,"line":348},[165,10309,10310],{"class":252},"      break",[165,10312,195],{"class":194},[165,10314,10315,10317,10319,10322,10324],{"class":167,"line":353},[165,10316,10287],{"class":252},[165,10318,184],{"class":183},[165,10320,10321],{"class":187},"stop-tracking",[165,10323,191],{"class":183},[165,10325,10094],{"class":194},[165,10327,10328,10331,10333],{"class":167,"line":373},[165,10329,10330],{"class":303},"      stopDomTracking",[165,10332,914],{"class":307},[165,10334,195],{"class":194},[165,10336,10337,10339],{"class":167,"line":387},[165,10338,10310],{"class":252},[165,10340,195],{"class":194},[165,10342,10343],{"class":167,"line":401},[165,10344,725],{"class":194},[165,10346,10347,10349,10351],{"class":167,"line":413},[165,10348,329],{"class":194},[165,10350,343],{"class":256},[165,10352,195],{"class":194},[2770,10354,10356],{"id":10355},"popup-ui","Popup UI",[1070,10358,10359,10362,10365],{},[105,10360,10361],{},"Temporary lifecycle",[105,10363,10364],{},"Rich UI capabilities",[105,10366,10367],{},"Dies when closed",[156,10369,10371],{"className":158,"code":10370,"language":160,"meta":48,"style":48},"\u002F\u002F Popup component\nfunction PopupApp() {\n  const sendMessage = async () => {\n    await browser.runtime.sendMessage({\n      target: \"background\",\n      action: \"start\",\n      data: {\n        \u002F* configuration *\u002F\n      },\n    });\n  };\n}\n",[162,10372,10373,10378,10389,10406,10426,10441,10456,10464,10469,10473,10481,10485],{"__ignoreMap":48},[165,10374,10375],{"class":167,"line":168},[165,10376,10377],{"class":233},"\u002F\u002F Popup component\n",[165,10379,10380,10382,10385,10387],{"class":167,"line":49},[165,10381,2783],{"class":171},[165,10383,10384],{"class":303}," PopupApp",[165,10386,914],{"class":194},[165,10388,1297],{"class":194},[165,10390,10391,10393,10396,10398,10400,10402,10404],{"class":167,"line":216},[165,10392,356],{"class":171},[165,10394,10395],{"class":5661}," sendMessage",[165,10397,180],{"class":179},[165,10399,5667],{"class":171},[165,10401,7731],{"class":194},[165,10403,761],{"class":171},[165,10405,1297],{"class":194},[165,10407,10408,10410,10413,10415,10417,10419,10422,10424],{"class":167,"line":237},[165,10409,5589],{"class":252},[165,10411,10412],{"class":256}," browser",[165,10414,46],{"class":194},[165,10416,9981],{"class":256},[165,10418,46],{"class":194},[165,10420,10421],{"class":303},"sendMessage",[165,10423,308],{"class":307},[165,10425,292],{"class":194},[165,10427,10428,10431,10433,10435,10437,10439],{"class":167,"line":243},[165,10429,10430],{"class":307},"      target",[165,10432,451],{"class":194},[165,10434,184],{"class":183},[165,10436,9867],{"class":187},[165,10438,191],{"class":183},[165,10440,384],{"class":194},[165,10442,10443,10446,10448,10450,10452,10454],{"class":167,"line":249},[165,10444,10445],{"class":307},"      action",[165,10447,451],{"class":194},[165,10449,184],{"class":183},[165,10451,9549],{"class":187},[165,10453,191],{"class":183},[165,10455,384],{"class":194},[165,10457,10458,10460,10462],{"class":167,"line":295},[165,10459,5526],{"class":307},[165,10461,451],{"class":194},[165,10463,1297],{"class":194},[165,10465,10466],{"class":167,"line":348},[165,10467,10468],{"class":233},"        \u002F* configuration *\u002F\n",[165,10470,10471],{"class":167,"line":353},[165,10472,5567],{"class":194},[165,10474,10475,10477,10479],{"class":167,"line":373},[165,10476,4614],{"class":194},[165,10478,343],{"class":307},[165,10480,195],{"class":194},[165,10482,10483],{"class":167,"line":387},[165,10484,1344],{"class":194},[165,10486,10487],{"class":167,"line":401},[165,10488,784],{"class":194},[2770,10490,10492],{"id":10491},"offscreen-documents","Offscreen Documents",[1070,10494,10495,10498,10501],{},[105,10496,10497],{},"Modern replacement for background pages",[105,10499,10500],{},"Handles tasks requiring DOM but no UI",[105,10502,10503],{},"Perfect for audio processing, canvas operations",[148,10505,10507],{"id":10506},"_2-common-pitfalls-and-solutions","2. Common Pitfalls and Solutions",[102,10509,10510],{},[105,10511,10512],{},[794,10513,10514],{},"Race Conditions",[156,10516,10518],{"className":158,"code":10517,"language":160,"meta":48,"style":48},"\u002F\u002F BAD: Fire and forget\nbrowser.runtime.sendMessage({ action: \"do_something\" });\n\n\u002F\u002F GOOD: Wait for response\nconst response = await browser.runtime.sendMessage({ action: \"do_something\" });\nif (response.success) {\n  \u002F\u002F Continue\n}\n",[162,10519,10520,10525,10559,10563,10568,10608,10623,10628],{"__ignoreMap":48},[165,10521,10522],{"class":167,"line":168},[165,10523,10524],{"class":233},"\u002F\u002F BAD: Fire and forget\n",[165,10526,10527,10529,10531,10533,10535,10537,10539,10541,10544,10546,10548,10551,10553,10555,10557],{"class":167,"line":49},[165,10528,10209],{"class":256},[165,10530,46],{"class":194},[165,10532,9981],{"class":256},[165,10534,46],{"class":194},[165,10536,10421],{"class":303},[165,10538,308],{"class":256},[165,10540,1611],{"class":194},[165,10542,10543],{"class":307}," action",[165,10545,451],{"class":194},[165,10547,184],{"class":183},[165,10549,10550],{"class":187},"do_something",[165,10552,191],{"class":183},[165,10554,4499],{"class":194},[165,10556,343],{"class":256},[165,10558,195],{"class":194},[165,10560,10561],{"class":167,"line":216},[165,10562,240],{"emptyLinePlaceholder":56},[165,10564,10565],{"class":167,"line":237},[165,10566,10567],{"class":233},"\u002F\u002F GOOD: Wait for response\n",[165,10569,10570,10572,10574,10576,10578,10580,10582,10584,10586,10588,10590,10592,10594,10596,10598,10600,10602,10604,10606],{"class":167,"line":243},[165,10571,172],{"class":171},[165,10573,4231],{"class":175},[165,10575,180],{"class":179},[165,10577,364],{"class":252},[165,10579,10412],{"class":256},[165,10581,46],{"class":194},[165,10583,9981],{"class":256},[165,10585,46],{"class":194},[165,10587,10421],{"class":303},[165,10589,308],{"class":256},[165,10591,1611],{"class":194},[165,10593,10543],{"class":307},[165,10595,451],{"class":194},[165,10597,184],{"class":183},[165,10599,10550],{"class":187},[165,10601,191],{"class":183},[165,10603,4499],{"class":194},[165,10605,343],{"class":256},[165,10607,195],{"class":194},[165,10609,10610,10613,10616,10618,10621],{"class":167,"line":249},[165,10611,10612],{"class":252},"if",[165,10614,10615],{"class":256}," (response",[165,10617,46],{"class":194},[165,10619,10620],{"class":256},"success) ",[165,10622,292],{"class":194},[165,10624,10625],{"class":167,"line":295},[165,10626,10627],{"class":233},"  \u002F\u002F Continue\n",[165,10629,10630],{"class":167,"line":348},[165,10631,784],{"class":194},[102,10633,10634],{"start":49},[105,10635,10636],{},[794,10637,10638],{},"Message Handler Memory Leaks",[156,10640,10642],{"className":158,"code":10641,"language":160,"meta":48,"style":48},"\u002F\u002F BAD: Listeners pile up\nfunction addListener() {\n  browser.runtime.onMessage.addListener(handler);\n}\n\n\u002F\u002F GOOD: Clean up listeners\nconst handler = (message) => {\n  \u002F* ... *\u002F\n};\nbrowser.runtime.onMessage.addListener(handler);\nreturn () => browser.runtime.onMessage.removeListener(handler);\n",[162,10643,10644,10649,10660,10685,10689,10693,10698,10717,10722,10726,10747],{"__ignoreMap":48},[165,10645,10646],{"class":167,"line":168},[165,10647,10648],{"class":233},"\u002F\u002F BAD: Listeners pile up\n",[165,10650,10651,10653,10656,10658],{"class":167,"line":49},[165,10652,2783],{"class":171},[165,10654,10655],{"class":303}," addListener",[165,10657,914],{"class":194},[165,10659,1297],{"class":194},[165,10661,10662,10664,10666,10668,10670,10672,10674,10676,10678,10681,10683],{"class":167,"line":216},[165,10663,9976],{"class":256},[165,10665,46],{"class":194},[165,10667,9981],{"class":256},[165,10669,46],{"class":194},[165,10671,9986],{"class":256},[165,10673,46],{"class":194},[165,10675,9991],{"class":303},[165,10677,308],{"class":307},[165,10679,10680],{"class":256},"handler",[165,10682,343],{"class":307},[165,10684,195],{"class":194},[165,10686,10687],{"class":167,"line":237},[165,10688,784],{"class":194},[165,10690,10691],{"class":167,"line":243},[165,10692,240],{"emptyLinePlaceholder":56},[165,10694,10695],{"class":167,"line":249},[165,10696,10697],{"class":233},"\u002F\u002F GOOD: Clean up listeners\n",[165,10699,10700,10702,10705,10707,10709,10711,10713,10715],{"class":167,"line":295},[165,10701,172],{"class":171},[165,10703,10704],{"class":5661}," handler",[165,10706,180],{"class":179},[165,10708,257],{"class":194},[165,10710,3579],{"class":536},[165,10712,343],{"class":194},[165,10714,761],{"class":171},[165,10716,1297],{"class":194},[165,10718,10719],{"class":167,"line":348},[165,10720,10721],{"class":233},"  \u002F* ... *\u002F\n",[165,10723,10724],{"class":167,"line":353},[165,10725,1515],{"class":194},[165,10727,10728,10730,10732,10734,10736,10738,10740,10742,10745],{"class":167,"line":373},[165,10729,10209],{"class":256},[165,10731,46],{"class":194},[165,10733,9981],{"class":256},[165,10735,46],{"class":194},[165,10737,9986],{"class":256},[165,10739,46],{"class":194},[165,10741,9991],{"class":303},[165,10743,10744],{"class":256},"(handler)",[165,10746,195],{"class":194},[165,10748,10749,10751,10753,10755,10757,10759,10761,10763,10765,10767,10770,10772],{"class":167,"line":387},[165,10750,10039],{"class":252},[165,10752,7731],{"class":194},[165,10754,761],{"class":171},[165,10756,10412],{"class":256},[165,10758,46],{"class":194},[165,10760,9981],{"class":256},[165,10762,46],{"class":194},[165,10764,9986],{"class":256},[165,10766,46],{"class":194},[165,10768,10769],{"class":303},"removeListener",[165,10771,10744],{"class":256},[165,10773,195],{"class":194},[102,10775,10776],{"start":216},[105,10777,10778],{},[794,10779,10780],{},"Context Death",[156,10782,10784],{"className":158,"code":10783,"language":160,"meta":48,"style":48},"\u002F\u002F BAD: Assuming context is always alive\n\u002F\u002F GOOD: Handle disconnects gracefully\ntry {\n  await browser.runtime.sendMessage({\n    \u002F* ... *\u002F\n  });\n} catch (error) {\n  if (error.message.includes(\"receiving end does not exist\")) {\n    \u002F\u002F Handle disconnected context\n  }\n}\n",[162,10785,10786,10791,10796,10802,10820,10825,10833,10843,10872,10877,10881],{"__ignoreMap":48},[165,10787,10788],{"class":167,"line":168},[165,10789,10790],{"class":233},"\u002F\u002F BAD: Assuming context is always alive\n",[165,10792,10793],{"class":167,"line":49},[165,10794,10795],{"class":233},"\u002F\u002F GOOD: Handle disconnects gracefully\n",[165,10797,10798,10800],{"class":167,"line":216},[165,10799,3490],{"class":252},[165,10801,1297],{"class":194},[165,10803,10804,10806,10808,10810,10812,10814,10816,10818],{"class":167,"line":237},[165,10805,742],{"class":252},[165,10807,10412],{"class":256},[165,10809,46],{"class":194},[165,10811,9981],{"class":256},[165,10813,46],{"class":194},[165,10815,10421],{"class":303},[165,10817,308],{"class":307},[165,10819,292],{"class":194},[165,10821,10822],{"class":167,"line":243},[165,10823,10824],{"class":233},"    \u002F* ... *\u002F\n",[165,10826,10827,10829,10831],{"class":167,"line":249},[165,10828,492],{"class":194},[165,10830,343],{"class":307},[165,10832,195],{"class":194},[165,10834,10835,10837,10839,10841],{"class":167,"line":295},[165,10836,329],{"class":194},[165,10838,3538],{"class":252},[165,10840,3541],{"class":256},[165,10842,292],{"class":194},[165,10844,10845,10847,10849,10851,10853,10855,10857,10859,10861,10863,10866,10868,10870],{"class":167,"line":348},[165,10846,589],{"class":252},[165,10848,257],{"class":307},[165,10850,3574],{"class":256},[165,10852,46],{"class":194},[165,10854,3579],{"class":256},[165,10856,46],{"class":194},[165,10858,558],{"class":303},[165,10860,308],{"class":307},[165,10862,191],{"class":183},[165,10864,10865],{"class":187},"receiving end does not exist",[165,10867,191],{"class":183},[165,10869,4111],{"class":307},[165,10871,292],{"class":194},[165,10873,10874],{"class":167,"line":353},[165,10875,10876],{"class":233},"    \u002F\u002F Handle disconnected context\n",[165,10878,10879],{"class":167,"line":373},[165,10880,725],{"class":194},[165,10882,10883],{"class":167,"line":387},[165,10884,784],{"class":194},[19,10886,9675],{"id":9674},[102,10888,10889,10895,10901],{},[105,10890,10891,10894],{},[794,10892,10893],{},"Start with TypeScript"," - Define your message types early. I started without it and regretted it.",[105,10896,10897,10900],{},[794,10898,10899],{},"Use a Message Bus Pattern"," - Centralize message handling logic instead of spreading it across files.",[105,10902,10903,10906],{},[794,10904,10905],{},"Build with Testing in Mind"," - Mock the messaging system for easier testing.",[15,10908,10909],{},"While I used WXT (a fantastic framework) for my extension, these principles apply to any browser extension. The framework handles the boilerplate, but understanding the underlying architecture is crucial.",[19,10911,9722],{"id":9721},[1070,10913,10914,10921,10928],{},[105,10915,10916],{},[34,10917,10920],{"href":10918,"rel":10919},"https:\u002F\u002Fdeveloper.chrome.com\u002Fdocs\u002Fextensions\u002Fmv3\u002Farchitecture-overview\u002F",[38],"Chrome Extension Architecture Overview",[105,10922,10923],{},[34,10924,10927],{"href":10925,"rel":10926},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FMozilla\u002FAdd-ons\u002FWebExtensions\u002FContent_scripts#communicating_with_background_scripts",[38],"Browser Extension Messaging Guide",[105,10929,10930,10935],{},[34,10931,10934],{"href":10932,"rel":10933},"https:\u002F\u002Fwxt.dev\u002F",[38],"WXT Framework"," - If you want a modern development experience",[1162,10937,10938],{},"html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sucvu, html code.shiki .sucvu{--shiki-light:#E53935;--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sfCm-, html code.shiki .sfCm-{--shiki-light:#90A4AE;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":48,"searchDepth":49,"depth":49,"links":10940},[10941,10942,10943,10947,10948],{"id":8932,"depth":49,"text":8933},{"id":9811,"depth":49,"text":9812},{"id":9828,"depth":49,"text":9829,"children":10944},[10945,10946],{"id":9832,"depth":216,"text":9833},{"id":10506,"depth":216,"text":10507},{"id":9674,"depth":49,"text":9675},{"id":9721,"depth":49,"text":9722},"2024-03-21","A little dive into browser extension messaging architecture, born from building a real SaaS product. Learn how different extension contexts communicate and how to architect your messaging system properly.",{"slug":10952},"browser-extension-messaging","\u002Fwriting\u002Fbrowser-extension-messaging",{"title":9795,"description":10950},"writing\u002Fbrowser-extension-messaging",[10957,10958,10959,64,10960],"Browser Extensions","Architecture","Chrome Extensions","WXT","X43s65cDHVO8rtdFvQTLHLdevTtARnL1vIEOYKPpPu8",1780955296665]