[{"data":1,"prerenderedAt":7138},["ShallowReactive",2],{"blog-list":3},[4,1003,1595,1754,2141,2575,3060,3436,4575,5935],{"id":5,"title":6,"body":7,"canonical":992,"date":993,"description":994,"draft":995,"duration":992,"extension":996,"lang":992,"meta":997,"navigation":196,"path":998,"place":992,"placeLink":992,"readingTime":999,"seo":1000,"stem":1001,"__hash__":1002},"blog\u002Fblog\u002Fhey-elvon.md","Hey Elvon!",{"type":8,"value":9,"toc":971},"minimark",[10,15,27,29,32,35,40,43,51,54,62,66,69,72,79,90,96,100,103,262,265,268,441,450,457,461,464,467,470,474,477,480,483,490,494,497,500,569,577,580,588,591,594,598,601,604,607,610,613,616,619,625,629,632,635,638,646,649,653,656,659,662,668,672,675,678,681,685,688,691,694,697,700,706,710,713,716,741,744,748,755,761,767,773,778,784,790,796,802,808,810,814,820,825,828,833,836,839,844,849,852,857,860,865,870,875,878,880,884,890,895,897,901,907,912,917,922,927,932,937,942,947,952,954,958,961,964,967],[11,12,14],"h3",{"id":13},"how-a-random-group-assignment-became-the-best-team-ive-worked-with-and-a-robot-that-actually-moves","How a random group assignment became the best team I've worked with, and a robot that actually moves.",[16,17,18],"p",{},[19,20],"img",{"alt":21,"src":22,"fetchPriority":23,"loading":24,"width":25,"height":26},"Elvon, the autonomous trash collector, on the lab table with its bin, wheels and exposed wiring","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Fcover.jpg","high","eager",2176,1080,[16,28,6],{},[16,30,31],{},"I didn't like working on school group projects. Simply because projects were usually left to one person to take the full brunt of the work. But I think I had the great opportunity of working with this amazing team in building Elvon.",[16,33,34],{},"The team selection was done in class at random, and we got assigned a trash collector. Every other project had the variation of line follower only, or obstacle detection.",[36,37,39],"h2",{"id":38},"meet-the-team","Meet the team",[16,41,42],{},"Five of us. Babatunde Yetunde (our group leader), Ogundipe Victor (circuit specialist), Ajayi John (our media director), Alonge Emmanuel (build and packaging), and Adebesin Tolulope (software guy, that's me).",[16,44,45],{},[19,46],{"alt":47,"src":48,"width":49,"height":50},"The team around the workbench, minus John, who was behind the camera","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Fteam.jpg",2000,991,[16,52,53],{},"Before any robotics project, we need to simulate and make a visual prototype, which Victor helped create for us to build freely.",[16,55,56],{},[19,57],{"alt":58,"src":59,"width":60,"height":61},"The Arduino, motor driver and ultrasonic sensor wired up on the breadboard for obstacle detection","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Fprototype.jpg",1500,1125,[36,63,65],{"id":64},"day-one-was-all-vibes","Day one was all vibes",[16,67,68],{},"For the first day, it was all vibes and chill. I remember it was so calm and everyone was so excited because it was going smoothly. We brought cameras, ready to take the occasion. And yeah, it did go smoothly.",[16,70,71],{},"And yeah, I tried to engage this time without being more passive. I was laughed at, obviously, so you don't know anything outside of working on your laptop (partly true). Because at the first point of touch, I didn't know what to do. I also have the fear of, well, electricity. We also tried to check if our glue gun was hot (yeah, I touched it and got a bit burnt, others claimed it wasn't hot). What are they feeding you guys?",[16,73,74],{},[19,75],{"alt":76,"src":77,"width":78,"height":78},"\"Just a little tap.\" Easy for him to say.","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Fglue-gun.gif",640,[16,80,81,82,89],{},"John took the major part of the camera work, he's a pro with cameras. Emmanuel and I started with the coupling, making sure we had the chassis ready and wheels for the more structural engineers to work here, Victor and Yetunde (their final year projects involved using Arduino, so they had more experience at it). And yeah, we got the very first setup. The motor connected and battery working as well. I ",[83,84,88],"a",{"href":85,"rel":86},"https:\u002F\u002Fx.com\u002FI_am_Lope\u002Fstatus\u002F2090907430687309843",[87],"nofollow","posted this"," as we were packing up for the day.",[16,91,92],{},[19,93],{"alt":94,"src":95,"width":60,"height":61},"The bare chassis on its wheels, motors wired, nothing else on it yet","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Fchassis.jpg",[36,97,99],{"id":98},"the-code","The code",[16,101,102],{},"We also wrote out the code for the robot.",[104,105,110],"pre",{"className":106,"code":107,"language":108,"meta":109,"style":109},"language-cpp shiki shiki-themes github-light github-dark","\u002F\u002F ==================== TIMINGS ====================\nconst unsigned long TURN_90_MS    = 500;   \u002F\u002F calibration knob: bump up if it under-rotates (\u003C ~90 deg)\nconst unsigned long REVERSE_MS    = 600;\nconst unsigned long PIR_WARMUP_MS = 30000;\n\n\u002F\u002F ==================== ROBOT STATES ====================\nenum RobotState {\n  PATROL,\n  PIR_WAIT,\n  OBSTACLE_AVOID\n};\n\nRobotState currentState = PATROL;\n","cpp","",[111,112,113,122,152,172,191,198,204,217,226,234,240,246,251],"code",{"__ignoreMap":109},[114,115,118],"span",{"class":116,"line":117},"line",1,[114,119,121],{"class":120},"sJ8bj","\u002F\u002F ==================== TIMINGS ====================\n",[114,123,125,129,132,135,139,142,146,149],{"class":116,"line":124},2,[114,126,128],{"class":127},"szBVR","const",[114,130,131],{"class":127}," unsigned",[114,133,134],{"class":127}," long",[114,136,138],{"class":137},"sVt8B"," TURN_90_MS    ",[114,140,141],{"class":127},"=",[114,143,145],{"class":144},"sj4cs"," 500",[114,147,148],{"class":137},";",[114,150,151],{"class":120},"   \u002F\u002F calibration knob: bump up if it under-rotates (\u003C ~90 deg)\n",[114,153,155,157,159,161,164,166,169],{"class":116,"line":154},3,[114,156,128],{"class":127},[114,158,131],{"class":127},[114,160,134],{"class":127},[114,162,163],{"class":137}," REVERSE_MS    ",[114,165,141],{"class":127},[114,167,168],{"class":144}," 600",[114,170,171],{"class":137},";\n",[114,173,175,177,179,181,184,186,189],{"class":116,"line":174},4,[114,176,128],{"class":127},[114,178,131],{"class":127},[114,180,134],{"class":127},[114,182,183],{"class":137}," PIR_WARMUP_MS ",[114,185,141],{"class":127},[114,187,188],{"class":144}," 30000",[114,190,171],{"class":137},[114,192,194],{"class":116,"line":193},5,[114,195,197],{"emptyLinePlaceholder":196},true,"\n",[114,199,201],{"class":116,"line":200},6,[114,202,203],{"class":120},"\u002F\u002F ==================== ROBOT STATES ====================\n",[114,205,207,210,214],{"class":116,"line":206},7,[114,208,209],{"class":127},"enum",[114,211,213],{"class":212},"sScJk"," RobotState",[114,215,216],{"class":137}," {\n",[114,218,220,223],{"class":116,"line":219},8,[114,221,222],{"class":144},"  PATROL",[114,224,225],{"class":137},",\n",[114,227,229,232],{"class":116,"line":228},9,[114,230,231],{"class":144},"  PIR_WAIT",[114,233,225],{"class":137},[114,235,237],{"class":116,"line":236},10,[114,238,239],{"class":144},"  OBSTACLE_AVOID\n",[114,241,243],{"class":116,"line":242},11,[114,244,245],{"class":137},"};\n",[114,247,249],{"class":116,"line":248},12,[114,250,197],{"emptyLinePlaceholder":196},[114,252,254,257,259],{"class":116,"line":253},13,[114,255,256],{"class":137},"RobotState currentState ",[114,258,141],{"class":127},[114,260,261],{"class":137}," PATROL;\n",[16,263,264],{},"Three states. That's the whole brain. Patrol until something is in the way, wait when a human shows up, avoid when the ultrasonic says there's a wall. No map, no path planning (more on why later).",[16,266,267],{},"And the ultrasonic read, which is about as simple as it gets:",[104,269,271],{"className":106,"code":270,"language":108,"meta":109,"style":109},"int readDistanceCm() {\n  digitalWrite(TRIG, LOW);\n  delayMicroseconds(2);\n\n  digitalWrite(TRIG, HIGH);\n  delayMicroseconds(10);\n\n  digitalWrite(TRIG, LOW);\n\n  unsigned long duration = pulseIn(ECHO, HIGH, 25000);\n\n  if (duration == 0) {\n    return 999;   \u002F\u002F nothing came back, treat as clear\n  }\n\n  return (int)(duration \u002F 58UL);\n}\n",[111,272,273,284,292,306,310,317,328,332,338,342,365,369,386,399,405,410,435],{"__ignoreMap":109},[114,274,275,278,281],{"class":116,"line":117},[114,276,277],{"class":127},"int",[114,279,280],{"class":212}," readDistanceCm",[114,282,283],{"class":137},"() {\n",[114,285,286,289],{"class":116,"line":124},[114,287,288],{"class":212},"  digitalWrite",[114,290,291],{"class":137},"(TRIG, LOW);\n",[114,293,294,297,300,303],{"class":116,"line":154},[114,295,296],{"class":212},"  delayMicroseconds",[114,298,299],{"class":137},"(",[114,301,302],{"class":144},"2",[114,304,305],{"class":137},");\n",[114,307,308],{"class":116,"line":174},[114,309,197],{"emptyLinePlaceholder":196},[114,311,312,314],{"class":116,"line":193},[114,313,288],{"class":212},[114,315,316],{"class":137},"(TRIG, HIGH);\n",[114,318,319,321,323,326],{"class":116,"line":200},[114,320,296],{"class":212},[114,322,299],{"class":137},[114,324,325],{"class":144},"10",[114,327,305],{"class":137},[114,329,330],{"class":116,"line":206},[114,331,197],{"emptyLinePlaceholder":196},[114,333,334,336],{"class":116,"line":219},[114,335,288],{"class":212},[114,337,291],{"class":137},[114,339,340],{"class":116,"line":228},[114,341,197],{"emptyLinePlaceholder":196},[114,343,344,347,349,352,354,357,360,363],{"class":116,"line":236},[114,345,346],{"class":127},"  unsigned",[114,348,134],{"class":127},[114,350,351],{"class":137}," duration ",[114,353,141],{"class":127},[114,355,356],{"class":212}," pulseIn",[114,358,359],{"class":137},"(ECHO, HIGH, ",[114,361,362],{"class":144},"25000",[114,364,305],{"class":137},[114,366,367],{"class":116,"line":242},[114,368,197],{"emptyLinePlaceholder":196},[114,370,371,374,377,380,383],{"class":116,"line":248},[114,372,373],{"class":127},"  if",[114,375,376],{"class":137}," (duration ",[114,378,379],{"class":127},"==",[114,381,382],{"class":144}," 0",[114,384,385],{"class":137},") {\n",[114,387,388,391,394,396],{"class":116,"line":253},[114,389,390],{"class":127},"    return",[114,392,393],{"class":144}," 999",[114,395,148],{"class":137},[114,397,398],{"class":120},"   \u002F\u002F nothing came back, treat as clear\n",[114,400,402],{"class":116,"line":401},14,[114,403,404],{"class":137},"  }\n",[114,406,408],{"class":116,"line":407},15,[114,409,197],{"emptyLinePlaceholder":196},[114,411,413,416,419,421,424,427,430,433],{"class":116,"line":412},16,[114,414,415],{"class":127},"  return",[114,417,418],{"class":137}," (",[114,420,277],{"class":127},[114,422,423],{"class":137},")(duration ",[114,425,426],{"class":127},"\u002F",[114,428,429],{"class":144}," 58",[114,431,432],{"class":127},"UL",[114,434,305],{"class":137},[114,436,438],{"class":116,"line":437},17,[114,439,440],{"class":137},"}\n",[16,442,443,444,449],{},"I think progressing from there was fully robotics. We worked on the obstacle avoidance, which I ",[83,445,448],{"href":446,"rel":447},"https:\u002F\u002Fx.com\u002FI_am_Lope\u002Fstatus\u002F2091286868742566077",[87],"shared on my X page",".",[451,452],"clip",{"alt":453,"height":454,"src":455,"width":456},"Elvon backing off a wall and picking a new direction",720,"\u002Fvideos\u002Fblog\u002Fhey-elvon\u002Fobstacle-avoidance.mp4",1280,[36,458,460],{"id":459},"then-everything-started-breaking","Then everything started breaking",[16,462,463],{},"One would think, oh yes, we're so done, let's get it. But that's when errors started surfacing, when we started to merge our other components together.",[16,465,466],{},"First was the problem that our motor was going in the opposite direction, hence no motion could be produced. Recalling that we made a judgement that we should alternate the wires around the motor board.",[16,468,469],{},"That was the tip of the iceberg.",[36,471,473],{"id":472},"the-pir-sensors-humbled-us","The PIR sensors humbled us",[16,475,476],{},"Merging the PIR sensors led to more real facing work (sensors are hard challenges). First it wasn't detecting properly. We wanted to use two sensors (for maximum efficiency), but it bit us in the back. Our robot was overstimulated, it sensed everything, and hence stopped at every point in time.",[16,478,479],{},"Before we also realized that we had maxed the output on the PIR sensors, so it had too long a range and that affected how it worked. After we figured that out, though a lot of times we had to edit our code thinking the code wasn't detecting as expected, it was the signals we were receiving that were not properly formed.",[16,481,482],{},"We removed one PIR and then we moved forward.",[16,484,485],{},[19,486],{"alt":487,"src":488,"width":26,"height":489},"The PIR sensor with its two potentiometers, sensitivity and delay, both turned all the way up","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Fpir-sensor.jpg",775,[36,491,493],{"id":492},"giving-it-a-name-and-a-voice","Giving it a name, and a voice",[16,495,496],{},"Now, a working robot. Let's add more human touch. Yetunde led the vision for the product here. We had to coin out the name for the robot. We called it Elvon.",[16,498,499],{},"So we added 3 audios to make the robot more human. Three tracks on the SD card, three moments:",[501,502,503,519],"table",{},[504,505,506],"thead",{},[507,508,509,513,516],"tr",{},[510,511,512],"th",{},"Track",[510,514,515],{},"When it plays",[510,517,518],{},"What Elvon says",[520,521,522,539,554],"tbody",{},[507,523,524,530,533],{},[525,526,527],"td",{},[111,528,529],{},"0001.mp3",[525,531,532],{},"On boot, before it starts roaming",[525,534,535],{},[536,537,538],"em",{},"\"Hi, I'm Elvon, your autonomous trash collector, made by CPE Group 7.\"",[507,540,541,546,549],{},[525,542,543],{},[111,544,545],{},"0002.mp3",[525,547,548],{},"A PIR sees a person",[525,550,551],{},[536,552,553],{},"\"Hi, kindly drop your trash in the waste bin.\"",[507,555,556,561,564],{},[525,557,558],{},[111,559,560],{},"0003.mp3",[525,562,563],{},"Bin-full sensor trips, robot parks",[525,565,566],{},[536,567,568],{},"\"Please empty the trash.\"",[16,570,571,572,449],{},"You can hear all three in the demo video: ",[83,573,576],{"href":574,"rel":575},"https:\u002F\u002Fyoutube.com\u002Fwatch?v=TODO",[87],"Elvon, full run",[16,578,579],{},"Now the robot would introduce itself after a warm boot, and then move (without a map, this is most efficient for motor robots). If it senses it's expected to work, it says the line out loud and holds still long enough for you to actually drop something.",[16,581,582,583,587],{},"And here's the part I'm still not over. The first version of that voice was ",[584,585,586],"strong",{},"me",". I recorded all three lines myself, and we actually shipped it, because the team said I sounded robotic. 😔 That was the compliment.",[16,589,590],{},"We only swapped it for a synthesised one because that one was louder, and in a room full of people you need the volume more than you need the authenticity.",[16,592,593],{},"But nah. Nothing made a sound.",[36,595,597],{"id":596},"two-nights-fighting-a-speaker","Two nights fighting a speaker",[16,599,600],{},"We thought okay, maybe the code. We went to the code and adjusted, and we did find out that the signals were probably polluting the way the speaker is meant to act, and the servo signal, because there was no edge case handled in the code. But the speaker still didn't work.",[16,602,603],{},"We had to wait till 9pm for 2 days trying to fight the speaker to work. We tried different techniques, using a resistor to bring the voltage down for it to maybe work for the DF module. We also tried to use maybe a 3.3V from the breadboard.",[16,605,606],{},"Oh, I forgot to mention that our board has a different design, hence the other side of the board wasn't even getting necessary voltage. We had to pass voltage from the battery across, and that helped, but the speaker didn't work.",[16,608,609],{},"We were almost giving up and switching to an LED, but we argued that's not efficient design. It's very easy to miss an LED, or even a screen, as not everyone would be watching a screen to see \"drop trash.\"",[16,611,612],{},"We tried troubleshooting and we got a lot of static. What could be wrong?",[16,614,615],{},"Eventually we figured it out. We connected one of the output pins of the DFPlayer to a common ground with the Arduino.",[16,617,618],{},"(It's the DFPlayer, though. Nothing was wrong with the speaker. Two nights. A ground wire.)",[16,620,621],{},[19,622],{"alt":623,"src":624},"The moment Elvon says its first line out loud","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Ffirst-voice.gif",[36,626,628],{"id":627},"teardown-four-days-out","Teardown, four days out",[16,630,631],{},"Now all was working, but we needed the robot to reverse and turn properly. That wasn't even working. We tuned the code, made more adjustments, fixed a lot.",[16,633,634],{},"4 days to our defense, we had to tear it all down again, from the ground, pick each piece and troubleshoot so it doesn't give any issue.",[16,636,637],{},"And yay, we got it running. Really running. And all that was left was packaging.",[16,639,640],{},[19,641],{"alt":642,"src":643,"width":644,"height":645},"Elvon fully disassembled on the table, every part laid out before the rebuild","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Fteardown.jpg",960,540,[16,647,648],{},"Emmanuel was solid here. He helped to ensure the proper packing of the robot, the design build.",[36,650,652],{"id":651},"the-bin-and-the-wire-that-burned","The bin, and the wire that burned",[16,654,655],{},"We wanted to make it more sophisticated by adding a servo motor to power the waste bin, but we ran into a few troubles. One, the bin we got was too heavy for the small body of the robot, hence the robot couldn't move.",[16,657,658],{},"On the same day, two days to our defense, the robot wire got burned pretty bad. We were able to isolate the problem quickly (we could have lost our project to a burn), (hey! I don't like hardware). Random burn, and we were just running tests.",[16,660,661],{},"But we didn't let that make us panic. We took an old charger and removed its pieces to make it work.",[16,663,664],{},[19,665],{"alt":666,"src":667},"The burnt wire, and the old charger we harvested to replace it","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Fburnt-wire.png",[36,669,671],{"id":670},"what-we-learned-about-power","What we learned about power",[16,673,674],{},"And yes it did work. We resumed packing and were able to establish a few principles.",[16,676,677],{},"It takes a lot of power to run the small build, which affects the way the robot turns around. We advise using 4 batteries, but you have to use a buck converter here so you don't burn your Arduino.",[16,679,680],{},"The power drains battery, hence it wouldn't do for a long stretch of work. TTF would probably be high. But it's rechargeable, so it's easy to work with in short bursts and you can get it powered quickly.",[36,682,684],{"id":683},"defense-day","Defense day",[16,686,687],{},"The day of defense, our charger module stopped working 😭. Why must this happen?",[16,689,690],{},"Somehow everyone was unfazed. I mean, we spent so many late nights here, tired, hungry. We made our video for the whole project from start to finish and hoped we didn't have to do a long demo that would require us turning.",[16,692,693],{},"And yeah! We defended it. Funny, we were ready to answer questions but our demo and video were impressive enough that we didn't even have to go through any questioning. Our lecturer was very impressed. The design, the implementation. And amazing thought to every component.",[16,695,696],{},"One simple piece of engineering here is: simple design, but make it work. Don't overcomplicate it.",[16,698,699],{},"And yes, the robot is working. And the team really is the best I've worked with so far, responsive and very active group.",[16,701,702],{},[19,703],{"alt":704,"src":705},"The team after defense, Elvon in front, everyone still standing","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Fdefense-day.png",[36,707,709],{"id":708},"lets-hear-from-the-team","Let's hear from the team",[16,711,712],{},"I couldn't write this and only give you my side of it. I sat each of them down and asked the same seven questions. Same questions, wildly different answers, which is the part I love.",[16,714,715],{},"Here's what I asked everyone:",[717,718,719,723,726,729,732,735,738],"ol",{},[720,721,722],"li",{},"What was the exact moment during the build when you realized your part of the system was actually going to hold together?",[720,724,725],{},"What was the worst late-night hardware or wiring disaster you had to debug when everyone else was half asleep?",[720,727,728],{},"If you could erase one component or bug from your memory forever, what would it be?",[720,730,731],{},"Looking back at the whole thing, what piece of the project are you secretly most proud that you pulled off?",[720,733,734],{},"If given another robotics project, would you love to participate?",[720,736,737],{},"What was the teamwork like for you?",[720,739,740],{},"Word of advice for other people that want to build theirs.",[742,743],"hr",{},[11,745,747],{"id":746},"victor-circuit-specialist","Victor, circuit specialist",[16,749,750],{},[83,751,754],{"href":752,"rel":753},"https:\u002F\u002Fx.com\u002FVictobiloba",[87],"Victor on X",[16,756,757],{},[19,758],{"alt":759,"src":760},"Victor mid-wiring, multimeter in hand","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Fvictor.png",[16,762,763,766],{},[584,764,765],{},"The moment it held together.","\nThere was a time everything stopped working, all the sensors. The moment we disconnected them and started all over again and they started working one after the other.",[16,768,769,772],{},[584,770,771],{},"The worst late night.","\nThe night Yetunde had us wait till the speaker worked and it didn't 😭",[16,774,775],{},[536,776,777],{},"(I want it on record that we did eventually beat the speaker. It just took a second night.)",[16,779,780,783],{},[584,781,782],{},"One thing to erase from memory.","\nIt would be the PIR sensor oo. Tweaking the potentiometer on it to adjust the sensitivity isn't easy, you can't really tell the effect.",[16,785,786,789],{},[584,787,788],{},"Most proud of.","\nI didn't do it alone, we all did, because the code, the hardware connection and all. But basically I'll say the operation of the entire robot.",[16,791,792,795],{},[584,793,794],{},"Would you do another one?","\nYes, if there's no strict deadline haha.",[16,797,798,801],{},[584,799,800],{},"The teamwork.","\nThe teamwork was perfect, we had the ideal team.",[16,803,804,807],{},[584,805,806],{},"Advice.","\nMake sure you have a good team where everyone is willing to pour in their contributions.",[742,809],{},[11,811,813],{"id":812},"john-media-director","John, media director",[16,815,816],{},[19,817],{"alt":818,"src":819},"John behind the camera, shooting the demo","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Fjohn.png",[16,821,822,824],{},[584,823,765],{},"\nFor me, that moment came when we finally powered the system and saw the different parts responding the way we had designed them to. Seeing the sensors communicate with the controller and the motors respond to the commands made everything feel real.",[16,826,827],{},"At that point, it wasn't just wires, components, and code anymore. It was becoming an actual robot. That was when I knew, \"Okay, this thing is really going to work.\"",[16,829,830,832],{},[584,831,771],{},"\nOne of the most frustrating moments was when the system suddenly refused to behave as expected, even though we were convinced the connections were correct.",[16,834,835],{},"We had to go through the wiring, power supply, sensors, motor connections, and code one after another. The funny part was that everyone was already tired, so every small problem felt ten times bigger.",[16,837,838],{},"Eventually, we discovered that the problem was related to a connection we had overlooked. Fixing it and seeing the system respond again was honestly one of the most satisfying moments of the entire build.",[16,840,841],{},[536,842,843],{},"(Every single one of our bugs was a connection we had overlooked. Every one.)",[16,845,846,848],{},[584,847,782],{},"\nThere were moments when one loose or misplaced connection could make us question everything we had done. You could spend a long time checking the code, only to eventually discover that the real problem was a simple hardware connection.",[16,850,851],{},"Robotics really teaches you not to assume that the problem is where you first think it is.",[16,853,854,856],{},[584,855,788],{},"\nI'm most proud of the fact that we were able to bring the different sections of the project together into one functioning system.",[16,858,859],{},"It wasn't just about making a motor move or getting a sensor to produce a reading. The real achievement was integrating the hardware, programming, sensing, movement, and collection mechanism so that they could work together toward one goal.",[16,861,862,864],{},[584,863,794],{},"\nThe project was challenging, but it was also a great learning experience. Robotics gives you the opportunity to combine programming, electronics, mechanical design, and problem-solving.",[16,866,867,869],{},[584,868,800],{},"\nEveryone had different strengths, and there were times when we didn't immediately agree on the best way to approach a problem. But that's also part of working as a team. We had to communicate, listen to different ideas, divide responsibilities, and help each other when something wasn't working.",[16,871,872,874],{},[584,873,806],{},"\nThe mistakes, debugging, late nights, and small victories are all part of what makes building a robot worthwhile.",[16,876,877],{},"At the end of the day, the biggest achievement isn't just having a working autonomous trash collector. It's knowing that you and your team took an idea, struggled through the challenges, learned along the way, and eventually turned that idea into something that could actually move and perform a task.",[742,879],{},[11,881,883],{"id":882},"yetunde-group-leader","Yetunde, group leader",[16,885,886],{},[19,887],{"alt":888,"src":889},"Yetunde with Elvon, the person who named it","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Fyetunde.png",[16,891,892],{},[536,893,894],{},"Answers coming, she's the one who named Elvon and led the vision for what it should feel like, so I'm not publishing this section half-done.",[742,896],{},[11,898,900],{"id":899},"emmanuel-build-and-packaging","Emmanuel, build and packaging",[16,902,903],{},[19,904],{"alt":905,"src":906},"Emmanuel packing the chassis, the part that made it look finished","\u002Fimages\u002Fblog\u002Fhey-elvon\u002Femmanuel.png",[16,908,909,911],{},[584,910,765],{},"\nIt was the exact moment we dropped the chassis on the floor with all the components inside, and nothing broke or fell out. When we turned on the power and the motor wheels actually moved, moving the whole weight smoothly without the wheels shaking or bending.",[16,913,914],{},[536,915,916],{},"(He says \"dropped\" like it was a planned test. It was not a planned test.)",[16,918,919,921],{},[584,920,771],{},"\nThat had to be around 9:00 PM when the ultrasonic sensor suddenly stopped detecting obstacles and the robot was just crashing straight into the wall.",[16,923,924,926],{},[584,925,782],{},"\nDefinitely, trying to get the speaker to work. It was really stressful troubleshooting that. At first we thought we got the wrong model of the DFPlayer.",[16,928,929,931],{},[584,930,788],{},"\nI'm secretly most proud of how the PIR sensor and the ultrasonic sensor smoothly work together, because part of the bug we faced is getting the PIR to differentiate an obstacle, which is handled by ultrasonic, from an actual human motion.",[16,933,934],{},[536,935,936],{},"(This is the one nobody outside the team would notice, and it's the hardest thing in the build. A wall and a person look the same to a PIR.)",[16,938,939,941],{},[584,940,794],{},"\nYeah, I would love to do it again.",[16,943,944,946],{},[584,945,800],{},"\nIt was a great experience, honestly. We had to heavily rely on each other. While I was focused on the chassis and the obstacle avoidance, others were handling the PIR motion sensor and the speaker audio. Communication was key, because if one person's code or wiring messed up, the whole robot would just park. We pushed each other through the frustrating moments.",[16,948,949,951],{},[584,950,806],{},"\nMy advice is simple: triple-check your power supply and use quality jumper wires. Most importantly, start small. Build the chassis and get the wheels moving first before you start adding all the fancy sensors.",[742,953],{},[36,955,957],{"id":956},"where-it-landed","Where it landed",[16,959,960],{},"Five people, one Arduino Uno, two PIR sensors (one of which we fired), an ultrasonic, a DFPlayer that only needed a ground wire, and a robot with a name.",[16,962,963],{},"If you're building yours: keep the design simple, keep the wiring accessible, and when the code looks wrong, check the ground first. 😅",[16,965,966],{},"And get the team right. That part isn't in the datasheet.",[968,969,970],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":109,"searchDepth":124,"depth":124,"links":972},[973,974,975,976,977,978,979,980,981,982,983,984,985,991],{"id":13,"depth":154,"text":14},{"id":38,"depth":124,"text":39},{"id":64,"depth":124,"text":65},{"id":98,"depth":124,"text":99},{"id":459,"depth":124,"text":460},{"id":472,"depth":124,"text":473},{"id":492,"depth":124,"text":493},{"id":596,"depth":124,"text":597},{"id":627,"depth":124,"text":628},{"id":651,"depth":124,"text":652},{"id":670,"depth":124,"text":671},{"id":683,"depth":124,"text":684},{"id":708,"depth":124,"text":709,"children":986},[987,988,989,990],{"id":746,"depth":154,"text":747},{"id":812,"depth":154,"text":813},{"id":882,"depth":154,"text":883},{"id":899,"depth":154,"text":900},{"id":956,"depth":124,"text":957},null,"2026-09-01","Five people, one random class assignment, and an autonomous trash collector we named Elvon. The wiring disasters, the two nights we lost to a speaker, the wire that burned two days before defense, and what the team had to say about it.",false,"md",{},"\u002Fblog\u002Fhey-elvon","12min",{"title":6,"description":994},"blog\u002Fhey-elvon","PVmpJWENIGrh7rPnZoqJ16Z2vzs86uJfDTcG_GT3rB0",{"id":1004,"title":1005,"body":1006,"canonical":992,"date":1588,"description":1589,"draft":995,"duration":992,"extension":996,"lang":992,"meta":1590,"navigation":196,"path":1591,"place":992,"placeLink":992,"readingTime":992,"seo":1592,"stem":1593,"__hash__":1594},"blog\u002Fblog\u002Fa-year-a-verse-and-an-ecosystem.md","A year, a verse and an ecosystem",{"type":8,"value":1007,"toc":1577},[1008,1012,1020,1029,1049,1057,1060,1063,1066,1069,1072,1105,1108,1111,1115,1118,1121,1124,1128,1131,1140,1149,1173,1176,1191,1263,1266,1286,1301,1304,1307,1332,1336,1339,1342,1345,1348,1366,1375,1379,1382,1385,1388,1392,1395,1408,1417,1420,1423,1426,1429,1432,1435,1445,1449,1452,1455,1458,1461,1465,1468,1474,1477,1480,1489,1492,1495,1503,1507,1510,1518,1521,1524,1544,1556,1565,1568,1571,1574],[11,1009,1011],{"id":1010},"a-year-in-the-chakraverse-what-i-built-what-i-threw-away-and-where-were-headed","A year in the chakraverse. What I built, what I threw away, and where we're headed.",[16,1013,1014],{},[19,1015],{"alt":1016,"src":1017,"fetchPriority":23,"loading":24,"width":1018,"height":1019},"A year, a verse and an ecosystem. The Panda, Chakra UI, Zag and Ark marks floating around the title on a warm cream background.","\u002Fimages\u002Fblog\u002Fa-year-a-verse-and-an-ecosystem\u002Fcover.svg",1200,630,[16,1021,1022,1023,1028],{},"A year ago, I was working as an IT student at a polytechnic networking department. Reasonably a very serene environment, I was able to meet every quota of work, my day job at ",[83,1024,1027],{"href":1025,"rel":1026},"https:\u002F\u002Fiq.wiki",[87],"IQ.wiki",", and my IT as well.",[16,1030,1031,1032,1037,1038,1043,1044,449],{},"During one of those days, I got a random email from ",[83,1033,1036],{"href":1034,"rel":1035},"https:\u002F\u002Fadebayosegun.com",[87],"Segun Adebayo",", popularly known as Sage, asking for a collaboration on ",[83,1039,1042],{"href":1040,"rel":1041},"https:\u002F\u002Fchakra-ui.com",[87],"Chakra UI"," and ",[83,1045,1048],{"href":1046,"rel":1047},"https:\u002F\u002Fzagjs.com",[87],"zag",[16,1050,1051],{},[19,1052],{"alt":1053,"src":1054,"width":1055,"height":1056},"An email from Oluwasegun Adebayo, dated Mon, Aug 25 2025 at 4:05 PM. It reads: Hey Tolulope, I came across your portfolio and I found it quite impressive. Saw that you use Chakra UI at IQ.Wiki which is great! I'm looking to grow the team of contributors to Chakra UI and thought you might be a good fit. Is this something you're open to? We provide: great onboarding and guidance so you don't feel lost, contribute in your free time (minimum ~5 hours every week), decent compensation for contributors depending on impact. Regards, Sage, Creator of Chakra UI.","\u002Fimages\u002Fblog\u002Fa-year-a-verse-and-an-ecosystem\u002Fsage-email.png",2352,802,[16,1058,1059],{},"Prior to that time my open source arc had been an on and off topic, but I genuinely still wanted to contribute meaningfully to the community. I had been looking for a dedicated project to keep extending, something to add to the body of open source. Relatively new, I had no idea what being a contributor actually was. It quite differs from what I'd known about.",[16,1061,1062],{},"And I should be honest about the first few weeks, because \"I had no idea\" is doing a lot of quiet work in that sentence. The first time I properly opened zag and chakra, I felt completely out of my depth. Machines calling machines, files pointing at files, and me sitting there trying to find where a single component even begins. I kept waiting for someone to notice I didn't really know what I was doing yet.",[16,1064,1065],{},"The thing that took me a while to see is that everybody is figuring it out as they go. The people I was intimidated by are also reading code they didn't write, also guessing, also wrong sometimes. They've just been doing it for longer, and they've stopped treating being lost as a sign they don't belong.",[16,1067,1068],{},"There's one idea I kept bumping into all year, and I only found the words for it much later, when I was writing a talk. Decide once, use everywhere. It sounds like a design system thing, and it is, but it turned out to be the thing underneath almost everything I worked on this year. So that's really what this is about.",[16,1070,1071],{},"Before I move on though, I'd like to tell a short story.",[16,1073,1074,1075,1080,1081,1085,1086,1085,1089,1085,1094,1085,1099,1104],{},"I had this colleague about 3-4 years ago at IQ.wiki, name is ",[1076,1077],"mention",{"handle":1078,"name":1079},"anubra266","Abraham Aremu",". I really respect him, and going through older conversations in the IQ.wiki team, he was everywhere (in a good and amazing way lol). He previously had been working on Chakra UI and the entire ecosystem, and he'd share his findings on our then WhatsApp group about ",[83,1082,1084],{"href":1040,"rel":1083},[87],"chakra",", ",[83,1087,1048],{"href":1046,"rel":1088},[87],[83,1090,1093],{"href":1091,"rel":1092},"https:\u002F\u002Fpandacss.com",[87],"panda",[83,1095,1098],{"href":1096,"rel":1097},"https:\u002F\u002Fark-ui.com",[87],"ark",[83,1100,1103],{"href":1101,"rel":1102},"https:\u002F\u002Fchoc-ui.com",[87],"choc ui"," (I recall I wanted to contribute to it but got overwhelmed).",[16,1106,1107],{},"Nonetheless, I think I found it interesting someone close to home was contributing to a widely used UI framework across the dev community. So by the time that email landed, I already had some picture in my head of what the inside of this thing looked like.",[16,1109,1110],{},"He's still at it, by the way. An Ark port for Ripple, a Panda ecosystem site. Which I find a bit funny now, that the person who made me think any of this was possible is building on the things I help maintain.",[36,1112,1114],{"id":1113},"the-machine-broke","The machine broke",[16,1116,1117],{},"It started pretty calm. I couldn't get started properly, my machine broke almost the same week I started setting up for chakra work 😄.",[16,1119,1120],{},"But I couldn't let that stop me. I had to gather out every ounce of money on me and got a new machine, a MacBook M4. And I think the work became smoother from then on. It started pretty slowly, being unsure of what to do or even how some things work.",[16,1122,1123],{},"But Sage was very kind, teaching every single step he applies to making decisions about building components. So my very work was mostly on Chakra UI.",[36,1125,1127],{"id":1126},"anatomy-and-the-components","Anatomy, and the components",[16,1129,1130],{},"If you go by what shipped, this part of the year was components. That's the surface though. A design system isn't a folder of components, the components are just the part you can see. The interesting bit is always the layer under them, and that's where most of the work actually went.",[16,1132,1133,1134,1139],{},"My very first contribution after was a ",[83,1135,1138],{"href":1136,"rel":1137},"https:\u002F\u002Fchakra-ui.com\u002Fdocs\u002Fcomponents\u002Faccordion#explorer",[87],"component explorer"," to show how anatomy makes up different components. Chakra components are built from parts, a root, a trigger, a positioner, a content, an item, and all of that used to live in a table in the docs. Now you can click a part and watch it light up in a live preview, with the slot recipe generated underneath, so you can see what you're styling before you style it.",[16,1141,1142,1143,1148],{},"Then I was able to work on a ",[83,1144,1147],{"href":1145,"rel":1146},"https:\u002F\u002Fchakra-ui.com\u002Fdocs\u002Fcomponents\u002Ftags-input",[87],"TagsInput"," next.",[16,1150,1151,1152,1085,1157,1085,1162,1167,1168,449],{},"A bit more components came around after. ",[83,1153,1156],{"href":1154,"rel":1155},"https:\u002F\u002Fchakra-ui.com\u002Fdocs\u002Fcomponents\u002Fcarousel",[87],"Carousel",[83,1158,1161],{"href":1159,"rel":1160},"https:\u002F\u002Fchakra-ui.com\u002Fdocs\u002Fcomponents\u002Fsplitter",[87],"Splitter",[83,1163,1166],{"href":1164,"rel":1165},"https:\u002F\u002Fchakra-ui.com\u002Fdocs\u002Fcomponents\u002Frich-text-editor",[87],"RTE"," based on ",[83,1169,1172],{"href":1170,"rel":1171},"https:\u002F\u002Ftiptap.dev",[87],"Tiptap",[16,1174,1175],{},"I enjoyed working on RTE cos we started out as making it a dedicated package on its own but changed it since perhaps it could just be a composition than a package 😄.",[16,1177,1178,1179,1184,1185,1190],{},"If you've not come across one before, a composition is what our docs call a ",[83,1180,1183],{"href":1181,"rel":1182},"https:\u002F\u002Fchakra-ui.com\u002Fdocs\u002Fcomponents\u002Favatar#closed-component",[87],"closed component",". Chakra components are open by default, every part is yours to place, which is lovely for flexibility, but it also means a ",[83,1186,1189],{"href":1187,"rel":1188},"https:\u002F\u002Fchakra-ui.com\u002Fdocs\u002Fcomponents\u002Ftooltip",[87],"Tooltip"," looks like this every single time you want one:",[104,1192,1196],{"className":1193,"code":1194,"language":1195,"meta":109,"style":109},"language-tsx shiki shiki-themes github-light github-dark","\u003CTooltip.Root>\n  \u003CTooltip.Trigger asChild>\n    \u003CButton>Hover me\u003C\u002FButton>\n  \u003C\u002FTooltip.Trigger>\n  \u003CPortal>\n    \u003CTooltip.Positioner>\n      \u003CTooltip.Content>\n        \u003CTooltip.Arrow \u002F>\n        Delete this item\n      \u003C\u002FTooltip.Content>\n    \u003C\u002FTooltip.Positioner>\n  \u003C\u002FPortal>\n\u003C\u002FTooltip.Root>\n","tsx",[111,1197,1198,1203,1208,1213,1218,1223,1228,1233,1238,1243,1248,1253,1258],{"__ignoreMap":109},[114,1199,1200],{"class":116,"line":117},[114,1201,1202],{},"\u003CTooltip.Root>\n",[114,1204,1205],{"class":116,"line":124},[114,1206,1207],{},"  \u003CTooltip.Trigger asChild>\n",[114,1209,1210],{"class":116,"line":154},[114,1211,1212],{},"    \u003CButton>Hover me\u003C\u002FButton>\n",[114,1214,1215],{"class":116,"line":174},[114,1216,1217],{},"  \u003C\u002FTooltip.Trigger>\n",[114,1219,1220],{"class":116,"line":193},[114,1221,1222],{},"  \u003CPortal>\n",[114,1224,1225],{"class":116,"line":200},[114,1226,1227],{},"    \u003CTooltip.Positioner>\n",[114,1229,1230],{"class":116,"line":206},[114,1231,1232],{},"      \u003CTooltip.Content>\n",[114,1234,1235],{"class":116,"line":219},[114,1236,1237],{},"        \u003CTooltip.Arrow \u002F>\n",[114,1239,1240],{"class":116,"line":228},[114,1241,1242],{},"        Delete this item\n",[114,1244,1245],{"class":116,"line":236},[114,1246,1247],{},"      \u003C\u002FTooltip.Content>\n",[114,1249,1250],{"class":116,"line":242},[114,1251,1252],{},"    \u003C\u002FTooltip.Positioner>\n",[114,1254,1255],{"class":116,"line":248},[114,1256,1257],{},"  \u003C\u002FPortal>\n",[114,1259,1260],{"class":116,"line":253},[114,1261,1262],{},"\u003C\u002FTooltip.Root>\n",[16,1264,1265],{},"And you'd be writing that again and again. So we close it up. One component, the parts wired inside, and the two things you'd actually change left as props:",[104,1267,1269],{"className":1193,"code":1268,"language":1195,"meta":109,"style":109},"\u003CTooltip content=\"Delete this item\">\n  \u003CButton>Hover me\u003C\u002FButton>\n\u003C\u002FTooltip>\n",[111,1270,1271,1276,1281],{"__ignoreMap":109},[114,1272,1273],{"class":116,"line":117},[114,1274,1275],{},"\u003CTooltip content=\"Delete this item\">\n",[114,1277,1278],{"class":116,"line":124},[114,1279,1280],{},"  \u003CButton>Hover me\u003C\u002FButton>\n",[114,1282,1283],{"class":116,"line":154},[114,1284,1285],{},"\u003C\u002FTooltip>\n",[16,1287,1288,1289,1296,1297,1300],{},"You add it with ",[83,1290,1293],{"href":1291,"rel":1292},"https:\u002F\u002Fchakra-ui.com\u002Fdocs\u002Fget-started\u002Fcli",[87],[111,1294,1295],{},"npx @chakra-ui\u002Fcli snippet add tooltip"," and it drops into your own ",[111,1298,1299],{},"components\u002Fui"," folder as plain source, so if you want the arrow gone, or a longer delay, or some variant nobody else asked for, you just open the file and change it. It's not an import from us. It's yours.",[16,1302,1303],{},"Which is the other half of decide once, use everywhere, and the half I didn't understand at first. Some decisions should be made once and shared, that's your tokens. But some of them aren't ours to make at all, and the honest thing is to hand you the source and get out of the way.",[16,1305,1306],{},"Which is why the RTE ended up there too. As a package we'd own that editor forever, and carry Tiptap's whole extension matrix along with it, and every \"can I add one more toolbar button\" would turn into a prop, then an API, then something we can't change later. As a composition you just run it and it's in your repo:",[104,1308,1312],{"className":1309,"code":1310,"language":1311,"meta":109,"style":109},"language-bash shiki shiki-themes github-light github-dark","npx @chakra-ui\u002Fcli snippet add rich-text-editor\n","bash",[111,1313,1314],{"__ignoreMap":109},[114,1315,1316,1319,1323,1326,1329],{"class":116,"line":117},[114,1317,1318],{"class":212},"npx",[114,1320,1322],{"class":1321},"sZZnC"," @chakra-ui\u002Fcli",[114,1324,1325],{"class":1321}," snippet",[114,1327,1328],{"class":1321}," add",[114,1330,1331],{"class":1321}," rich-text-editor\n",[36,1333,1335],{"id":1334},"the-codemod","The codemod",[16,1337,1338],{},"Few things after was to build a codemod for people coming from v2 to v3.",[16,1340,1341],{},"It was quite the experience. I took mostly inspiration from the Next.js codemod, and eventually we were able to make it work, tested it across several v2 projects till we were fully satisfied it was working. And it is working today.",[16,1343,1344],{},"Same idea again, if you squint. Decide how a v2 Modal becomes a v3 one exactly once, write that decision down as code, and then it runs everywhere instead of living in somebody's head for four hundred files.",[16,1346,1347],{},"Doing a major upgrade by hand is tiring. You rename the props, you convert a Modal to its compound form, you fix the theme, then you do it again everywhere, and somewhere along the line you start missing things. So it's one command instead, and you can look at what it wants to do before you let it:",[104,1349,1351],{"className":1309,"code":1350,"language":1311,"meta":109,"style":109},"npx @chakra-ui\u002Fcodemod upgrade --dry\n",[111,1352,1353],{"__ignoreMap":109},[114,1354,1355,1357,1360,1363],{"class":116,"line":117},[114,1356,1318],{"class":212},[114,1358,1359],{"class":1321}," @chakra-ui\u002Fcodemod",[114,1361,1362],{"class":1321}," upgrade",[114,1364,1365],{"class":144}," --dry\n",[16,1367,1368,1369,1374],{},"The ",[83,1370,1373],{"href":1371,"rel":1372},"https:\u002F\u002Fchakra-ui.com\u002Fdocs\u002Fget-started\u002Fmigration",[87],"migration guide"," has the rest of it.",[36,1376,1378],{"id":1377},"the-part-i-wasnt-proud-of","The part I wasn't proud of",[16,1380,1381],{},"Moving on, it was to attend to a few features in the pro system. I didn't have much fun there 🫠.",[16,1383,1384],{},"Maybe I did, but I felt majority of the work I did there weren't really pretty and I was really hard on myself after 😔.",[16,1386,1387],{},"But nonetheless, we got people the ability to invite to teams licensing.",[36,1389,1391],{"id":1390},"chakra-v4-and-the-turn-to-panda","Chakra v4, and the turn to Panda",[16,1393,1394],{},"Around this time chakra v4 started becoming a conversation, and a tour component.",[16,1396,1397,1398,1401,1402,1407],{},"For chakra v4, we had so many different ideas, of which I did do some little experiments (wonder if to share them here), but it was a baseline start, and the direction we're heading towards lives here. Many have been asking us the same question for a while: ",[536,1399,1400],{},"\"When will chakra become build time?\""," Sage had already written about where this was going in ",[83,1403,1406],{"href":1404,"rel":1405},"https:\u002F\u002Fadebayosegun.com\u002Fblog\u002Fthe-future-of-chakra-ui",[87],"The future of Chakra UI",", and one line in there has basically been our to-do list.",[16,1409,1410,1411,1416],{},"After more thoughts, Sage decided let's move that up. Let's make ",[83,1412,1415],{"href":1413,"rel":1414},"https:\u002F\u002Fv2.pandacss.com",[87],"Panda CSS"," v2.",[16,1418,1419],{},"Now v2 was to bring a lot of grace to v1 (really awesome job on there). v2 was mostly inspired to improve the very said nuance v1 faced. One was speed, and a couple of ecosystem requested features.",[16,1421,1422],{},"During the iterations, I unceremoniously got invited to be a maintainer at Panda v2 (I did make myself a confetti 🎉).",[16,1424,1425],{},"I think v2 has progressed so much and I'm very excited about the upcoming release. The team has been working so hard in making it very much smooth, especially in building a design system.",[16,1427,1428],{},"And Panda is where decide once, use everywhere stops being a nice phrase and becomes an actual config field. You build your tokens and recipes in one package, point an app at it with a single key, and the app stops regenerating a system it only wanted to use.",[16,1430,1431],{},"The speed reveals on large projects. On a 100-file project, a cold parse of every file went from 177ms to 7.6ms. The warm re-parse of a single file, the watch-mode hot path, went from 652µs to 1.8µs (Hey, don't be a nerd!). That second one is the gap between saving a file and seeing the style, and it's small enough now to disappear.",[16,1433,1434],{},"Personally, I'd say we're here already. PandaCSS v2 is here and here to stay. And you can try it out yourself today.",[16,1436,1437,1442],{},[19,1438],{"alt":1439,"src":1440,"width":1018,"height":1441},"Two bars comparing a cold parse of 100 files. The v1 ts-morph pipeline fills the whole track at 177ms. The v2 Rust compiler is a small dot at 7.6ms.","\u002Fimages\u002Fblog\u002Fa-year-a-verse-and-an-ecosystem\u002Fspeed.svg",420,[536,1443,1444],{},"Same config, same 100 files. The numbers come straight out of the bench report in the repo, so you can rerun them yourself.",[36,1446,1448],{"id":1447},"late-nights","Late nights",[16,1450,1451],{},"I'd say the late nights are mostly making sure we have lesser APIs, and thinking about how users would feel using it.",[16,1453,1454],{},"That's deciding if we want the user to be the one making this styling decision, and not being opinionated about designs. Deciding once is only good when it's the right decision to be making on someone's behalf, and a lot of the job is working out which ones aren't. Making sure it doesn't force a model, and that the model we do choose is simply accessible. Be able to create UI with minimal code as possible.",[16,1456,1457],{},"The other one is just fighting with types and experimenting.",[16,1459,1460],{},"So it's rarely some heroic debugging session, it's mostly sitting with an API for hours trying to make it smaller. I built a scheduler machine in zag this year and put a whole resource concept in it, because obviously a scheduler needs resources right? Then I took it out again, nothing was using it yet.",[36,1462,1464],{"id":1463},"the-mindset-shift","The mindset shift",[16,1466,1467],{},"It's quite a lot. One is that you need to read a lot and understand things before making a comment or answering a discussion, before deciding what to work on, before picking up an issue.",[16,1469,1470,1471],{},"In the past I'd ask Sage how he came about the design thoughts and the inspiration, and he gave the one answer everyone gives. ",[536,1472,1473],{},"\"From years of experience.\"",[16,1475,1476],{},"I understand it now. Largely it's also picking out what shapes the ecosystem. In the age of AI where we can simply build just anything and everything, you put more thoughts into delivering the best for users, and you don't break anything.",[16,1478,1479],{},"My PR review strategy became almost same as what Sage teaches haha.",[16,1481,1482,1483,1488],{},"Around the ecosystem, off Panda I do reviews and answer discussions across the ",[83,1484,1487],{"href":1485,"rel":1486},"https:\u002F\u002Fchakraverse.dev",[87],"chakraverse",". Not zag yet (feels like so much going on 😂), but interestingly my first ever contribution was to zag, before I even got an invite to collaborate with Sage.",[16,1490,1491],{},"These days it looks like leaving direction notes on somebody else's pull request instead of opening my own. There's a POC for v4 that someone else started, and my actual job on it was to read it properly, write down where I thought it should go, and then ask if we could align instead of pulling in two directions. A year ago I would not have thought that was my place to do.",[16,1493,1494],{},"So somewhere along the line it turned from me asking the questions to me answering them, and I didn't really notice it happen.",[16,1496,1497,1498,449],{},"Moving on, I think the rosy part of conversations was then becoming a maintainer at Chakra UI as well. Being in charge of releases and ",[83,1499,1502],{"href":1500,"rel":1501},"https:\u002F\u002Fnesbitt.io\u002F2026\u002F08\u002F28\u002Fnow-hiring-senior-open-source-maintainer.html",[87],"every other thing a maintainer does",[36,1504,1506],{"id":1505},"where-would-i-be-going-from-here","Where would I be going from here?",[16,1508,1509],{},"ARK V6!!! (Someone's excited)",[16,1511,1512],{},[19,1513],{"alt":1514,"src":1515,"width":1516,"height":1517},"A GitHub notification reading \"@segunadebayo has invited you to collaborate on the chakra-ui\u002Fark repository\", with Lope's avatar and Sage's avatar side by side, joined by a plus sign.","\u002Fimages\u002Fblog\u002Fa-year-a-verse-and-an-ecosystem\u002Fark-invite.png",1484,642,[16,1519,1520],{},"Which is around when it stopped feeling like four separate projects I was helping out on, and started feeling like one thing.",[16,1522,1523],{},"To properly work on Chakra UI v4, we have to do a lot of upstream work to make sure Chakra UI users get the very best, of which I'm pretty sold on.",[16,1525,1526,1527,1531,1532,1531,1536,1539,1540,1543],{},"And our direction is very simple. ",[83,1528,1530],{"href":1413,"rel":1529},[87],"Panda"," v2, ",[83,1533,1048],{"href":1534,"rel":1535},"https:\u002F\u002Fv2.zagjs.com\u002F",[87],[83,1537,1098],{"href":1096,"rel":1538},[87]," v6, ",[83,1541,1084],{"href":1040,"rel":1542},[87]," v4. In that order, cos the ones on top can't really move till the ones under them do.",[16,1545,1546,1547,1550,1551,449],{},"Panda v2 is the one I'm really excited about. Try the beta, play with it, and don't go all ",[536,1548,1549],{},"beta could be bitter",". No, the beta we have here is better, people are already building on it. If you find anything wrong, ",[83,1552,1555],{"href":1553,"rel":1554},"https:\u002F\u002Fgithub.com\u002Fchakra-ui\u002Fpanda\u002Fdiscussions\u002F3599",[87],"tell us here",[16,1557,1558],{},[584,1559,1560,1561,449],{},"Phew! A busy year in ",[83,1562,1487],{"href":1563,"rel":1564},"https:\u002F\u002Fchakraverse.dev\u002F",[87],[16,1566,1567],{},"In all of this, I think I perhaps might have forgotten how I totally much started. But the most exciting thing here has been working at Chakra UI, and every project, and for one I'm totally grateful for the privilege to be able to give back to the dev community and also learn from the dev community.",[16,1569,1570],{},"And more is coming. We'll keep improving every of the projects you use today, for you to build better relationships with UIs.",[16,1572,1573],{},"So go build something you're proud of. And when you do, come tell me about it. I'll be around.",[968,1575,1576],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":109,"searchDepth":124,"depth":124,"links":1578},[1579,1580,1581,1582,1583,1584,1585,1586,1587],{"id":1010,"depth":154,"text":1011},{"id":1113,"depth":124,"text":1114},{"id":1126,"depth":124,"text":1127},{"id":1334,"depth":124,"text":1335},{"id":1377,"depth":124,"text":1378},{"id":1390,"depth":124,"text":1391},{"id":1447,"depth":124,"text":1448},{"id":1463,"depth":124,"text":1464},{"id":1505,"depth":124,"text":1506},"2026-08-31","A year inside the chakraverse. The components, the codemod, the pro system work I wasn't proud of, and what a year of building for other developers taught me.",{},"\u002Fblog\u002Fa-year-a-verse-and-an-ecosystem",{"title":1005,"description":1589},"blog\u002Fa-year-a-verse-and-an-ecosystem","TyiWeD70BFF6Rig588N4yZv18OQbgasBUxIeg-pRyBs",{"id":1596,"title":1597,"body":1598,"canonical":992,"date":1747,"description":1748,"draft":995,"duration":992,"extension":996,"lang":992,"meta":1749,"navigation":196,"path":1750,"place":992,"placeLink":992,"readingTime":992,"seo":1751,"stem":1752,"__hash__":1753},"blog\u002Fblog\u002Falmost-got-got.md","Almost got got",{"type":8,"value":1599,"toc":1745},[1600,1608,1611,1614,1617,1641,1644,1647,1650,1653,1656,1659,1662,1665,1668,1671,1674,1677,1680,1683,1686,1689,1700,1708,1711,1717,1724,1727,1730,1733,1736,1739,1742],[16,1601,1602],{},[19,1603],{"alt":1604,"src":1605,"fetchPriority":23,"loading":24,"width":1606,"height":1607},"Almost Got Got, a field guide to social engineering","\u002Fimages\u002Fblog\u002Falmost-got-got\u002Fog.png",2400,1260,[16,1609,1610],{},"Security is important.",[16,1612,1613],{},"Well, this is another project to wrap up my last day at Forcados.",[16,1615,1616],{},"I had already prepared a slide some time during the semester for our fair project.",[1618,1619,1620,1625,1632,1638],"blockquote",{},[16,1621,1622],{},[584,1623,1624],{},"CPE 510 - COMPUTER SECURITY TECHNIQUES",[16,1626,1627,1628,1631],{},"Page 360 of Network Security Essentials",[1629,1630],"br",{},"\nSub-Topic: Malware countermeasure approaches",[16,1633,1634,1635],{},"Assignment: There are four main elements of prevention listed by SP 800-83: ",[536,1636,1637],{},"Policy, Awareness, Vulnerability Mitigation, Threat Mitigation.",[16,1639,1640],{},"Divide yourselves into four groups, one element per group and do something educative or presentable with the element assigned to your group, it can even go beyond the classroom, towards the whole school.",[16,1642,1643],{},"The experience was mostly personal. I was trying to download a sermon, and I think it was on the old site, and yeah, there was some weird behavior. I saw an Apple boot screen, so identical, and then I was told to copy some things into the terminal to make the window go away. In a panic, I almost fell for the trick till I researched it. 😅",[16,1645,1646],{},"When our CPE 510 group got handed \"Malware Countermeasure Approaches\", specifically SP 800-83's four elements, the first thing I noticed was how useless every textbook definition felt next to what had just almost gotten me. So I started where everyone starts, the team slide. Then I went a step further and put together an actual policy document for FUTA, a proper Malware Prevention Policy. It was solid. But it didn't feel right. It was still just us talking at people, and it needed more of the community in it.",[16,1648,1649],{},"That's when the idea clicked: gather real stories from real people, and build the thing around them. I took it to the group and they agreed in unison. And watching how the other groups were approaching their elements, I already knew what I didn't want. We shouldn't make policy boring. We should make it fun, something that leaves a memory.",[16,1651,1652],{},"So I made two forms, one for the inner team and one for the public. Eleven came back from the team, twelve from outside. Twenty-three stories. ❤️ And the wild part wasn't the number, it was how close to home all of it was. Friends. Neighbours. People a text away. Appreciate everyone who shared. And once we lined the stories up, the pattern was impossible to miss.",[16,1654,1655],{},"The core was mostly social engineering, which is a prevalent issue in the modern world. In the developer space, we've been having a lot of supply chain attacks and even malware, GitHub ones, npm ones. It's been a really crazy 2026. Lots of tools have been developed to track these; thankful to Sentry today for their immense efforts, and the npmx community for one.",[16,1657,1658],{},"So here's the thing about those four elements. Awareness, Vulnerability Mitigation, Threat Mitigation, those three are mostly about machines. Patch this, scan that, filter the other. But Policy? Policy is about people. And that's exactly why I didn't want us to write the usual \"users shall not...\" document that nobody reads and everybody forgets.",[16,1660,1661],{},"Because think about what actually happened to me. No virus caught me. No firewall failed. A website just... asked me to do something, and I almost did it. That's social engineering. There's no antivirus for a human being deciding to trust the wrong thing. The only thing that stops it is you knowing the trick before it reaches you.",[16,1663,1664],{},"So that became our whole idea: a playbook, not a rulebook.",[16,1666,1667],{},"Instead of listing rules, we list the moves. The way the scammer plays it, and the one line that beats it. Like:",[16,1669,1670],{},"The move: someone calls saying they're from ICT, and your account gets shut down unless you confirm your password right now.\nThe counter: ICT will never ask for your password. Ever. Hang up.",[16,1672,1673],{},"Same information a boring policy would give you. But this one you'll actually remember when your phone rings.",[16,1675,1676],{},"That's where the forms came in. I didn't want to sit and invent scams from a textbook, I wanted real ones. So the inner form is for my team, twelve of us, each person owns one \"play,\" researches it, and writes the move and the counter. The public form is for everyone else, the whole school and beyond, just tell us your story. Have you ever been scammed, or nearly? What happened?",[16,1678,1679],{},"And people showed up ❤️. Honestly, it was a little scary seeing how common all of it is.",[16,1681,1682],{},"The pattern was almost always the same. Someone pretending to be your bank, or a person you trust, and a push to move money or read out a code right now. Not one of the stories was a real hack. Every single one was just... persuasion. That's the whole thing. The vulnerability was trust.",[16,1684,1685],{},"Getting it to feel right took a while. More sessions than I'll admit, going back and forth on how it should look and how it should read. A slide deck? A poster? A PDF nobody opens? We kept pulling it apart and putting it back together until it became two things in one: a playbook you can scroll on your phone, and a field manual you can flip through like a little book. The design mattered as much as the words, because a policy nobody enjoys reading is a policy nobody reads.",[16,1687,1688],{},"The plan is to put it all together as a webpage. One link. Something you can open on your phone, read in three minutes, and actually enjoy reading. Fun on the surface, but real underneath, because there's still a proper policy sitting under the playbook, the kind FUTA ICT could actually adopt if they wanted to.",[16,1690,1691,1692,1699],{},"It's live here: ",[584,1693,1694],{},[83,1695,1698],{"href":1696,"rel":1697},"https:\u002F\u002Falmostgotgot.vercel.app",[87],"almostgotgot.vercel.app",". Fifteen plays, the habits that beat them, and the real cases behind each.",[16,1701,1702],{},[19,1703],{"alt":1704,"src":1705,"width":1706,"height":1707},"The playbook, a field guide to social engineering","\u002Fimages\u002Fblog\u002Falmost-got-got\u002Fhero.png",2880,1880,[16,1709,1710],{},"You can read it straight down the page, or flip through the whole thing like a book.",[16,1712,1713],{},[19,1714],{"alt":1715,"src":1716,"width":1706,"height":1707},"The book view","\u002Fimages\u002Fblog\u002Falmost-got-got\u002Fbook-cover.png",[16,1718,1719],{},[19,1720],{"alt":1721,"src":1722,"width":1706,"height":1723},"The habits, and the plays","\u002Fimages\u002Fblog\u002Falmost-got-got\u002Foverview.png",7200,[16,1725,1726],{},"Funny how it started. I was just trying to download a sermon. 😅 Almost got got. And instead of keeping it to myself, it turned into a thing that might stop someone else from almost getting got too. That's the part I like. Not a policy in a folder. Something people remember.",[16,1728,1729],{},"And while this might feel like a school project, security, the kind of threats we're facing now, is your responsibility and mine. It might not affect you today. It might be something far off you read about in the news. But some of it gets closer. Someone around you could get hit by a supply chain attack, dev-related, that quietly gains access, and by that law of the universal circle where everyone seems connected, you could end up part of the attack too.",[16,1731,1732],{},"So keep your passwords carefully. Guard your privacy as much as you guard everything else. Your network, your internet, guard those too. Open links you trust. If you don't trust one, view it through something that keeps it away from your PC. And if you must run code, run it in a virtual machine.",[16,1734,1735],{},"Don't spin the wheel to claim some iPhones. Don't open that link. Don't download that free game. Protect your friends.",[16,1737,1738],{},"Here's what I keep coming back to, as someone who builds things: security isn't a compliance checklist. It's a UX problem. If the safe path isn't also the easy, obvious, memorable one, people will keep pasting the command from the fake Apple boot screen. Every time. That's not their failure, it's a design failure. So we designed the safe path to be the one you actually remember.",[16,1740,1741],{},"And I really do believe the internet can be made safe. Not by some big company, not by one clever tool, though those help. It begins with you and me, and the small careful choices we make every day.",[16,1743,1744],{},"Let's make the web, the internet, a safe place again.",{"title":109,"searchDepth":124,"depth":124,"links":1746},[],"2026-08-23","I was just trying to download a sermon, and I almost pasted malware into my terminal. That near-miss turned into our CPE 510 project, and a small crusade to make security policy something people actually remember.",{},"\u002Fblog\u002Falmost-got-got",{"title":1597,"description":1748},"blog\u002Falmost-got-got","K5hlWQcSThorEMFEh9uzriceu6pN47VtUUlGKA-un-E",{"id":1755,"title":1756,"body":1757,"canonical":992,"date":2133,"description":2134,"draft":995,"duration":992,"extension":996,"lang":992,"meta":2135,"navigation":196,"path":2136,"place":992,"placeLink":992,"readingTime":2137,"seo":2138,"stem":2139,"__hash__":2140},"blog\u002Fblog\u002Fmy-pastor-once-said.md","My Pastor Once Said",{"type":8,"value":1758,"toc":2120},[1759,1763,1770,1784,1800,1804,1816,1823,1826,1830,1837,1840,1846,1857,1861,1864,1871,1877,1880,1884,1898,1905,1908,1919,1925,1929,1932,1939,1942,1946,1949,1952,1994,1997,2001,2012,2015,2026,2034,2038,2041,2050,2053,2060,2064,2067,2073,2075,2078,2086,2109,2117],[11,1760,1762],{"id":1761},"a-campaign-for-the-lines-that-stuck-what-i-designed-what-broke-what-the-numbers-said-and-where-it-landed","A campaign for the lines that stuck. What I designed, what broke, what the numbers said, and where it landed.",[16,1764,1765],{},[19,1766],{"alt":1767,"src":1768,"fetchPriority":23,"loading":24,"width":1706,"height":1769},"The My Pastor Once Said landing page, a wall of quote cards fanning out around the words \"My Pastor Once Said…\" in BethelFlow blue.","\u002Fimages\u002Fblog\u002Fmy-pastor-once-said\u002Fhero.png",1800,[16,1771,1772,1773,1776,1777,1776,1780,1783],{},"Every church has a few lines it never forgot. The one your pastor said at the end of every service. The sentence you were half-listening to at seventeen that you're still carrying at twenty-seven. ",[536,1774,1775],{},"Shalom."," ",[536,1778,1779],{},"You are not a mistake.",[536,1781,1782],{},"Shout hallelujah."," You know the ones.",[16,1785,1786,1787,1793,1794,1799],{},"We wanted to collect them. Not in a database, but on people's phones, in group chats, on statuses. So we built ",[83,1788,1791],{"href":1789,"rel":1790},"https:\u002F\u002Fwww.bethelflow.com\u002Fmy-pastor-once-said",[87],[584,1792,1756],{}," for ",[83,1795,1798],{"href":1796,"rel":1797},"https:\u002F\u002Fwww.bethelflow.com\u002F",[87],"BethelFlow",": type a line your pastor never stopped saying, add a photo, and walk out with a card built to pass on. This is the story of designing it. The idea, my part in it, the rough edges, and where it is now.",[36,1801,1803],{"id":1802},"borrowing-a-good-idea","Borrowing a good idea",[16,1805,1806,1807,1085,1812,1815],{},"The seed was ",[83,1808,1811],{"href":1809,"rel":1810},"https:\u002F\u002Fcowrywise.com\u002Fmothers-day",[87],"Cowrywise's Mother's Day page",[536,1813,1814],{},"\"Mama once said…\"",", a small, joyful thing that turned a feeling everyone already had into something you could make and send in under a minute. We wanted that shape, pointed at the people who shaped our faith.",[16,1817,1818,1819,1822],{},"The strategy underneath it is plain. The movement is the hook, and ",[536,1820,1821],{},"\"start a church on BethelFlow\""," is the quiet conversion sitting under it. Top of funnel dressed as a keepsake. That framing mattered, because it set the bar for the whole thing: it had to be the kind of page a person shares for their own reasons, not because we asked.",[16,1824,1825],{},"My part was the experience. Make it fast to pick up, cheap to run, and good enough that sharing it never feels like an ad.",[36,1827,1829],{"id":1828},"designing-for-a-five-second-pickup","Designing for a five-second pickup",[16,1831,1832,1833,1836],{},"The rule I set myself: a stranger should understand what this is and start making one before they've decided whether to. That meant the payoff couldn't live at the end of a form. It had to be ",[536,1834,1835],{},"right there"," while you typed.",[16,1838,1839],{},"So the create flow is a small wizard, the line, then the photo, then the card, with a live preview sitting next to the form the whole time. Cowrywise reveals the card after you finish. We diverged and let you watch it assemble as you go. You type a name, it appears on the card. You paste the quote, the card breathes. The work and the reward share a screen.",[16,1841,1842],{},[19,1843],{"alt":1844,"src":1845,"width":1706,"height":1769},"Step one of the create flow: a form asking \"What did they always say?\" beside a live preview of the card updating in real time.","\u002Fimages\u002Fblog\u002Fmy-pastor-once-said\u002Fcompose-line.png",[16,1847,1848,1849,1852,1853,1856],{},"The whole thing wears BethelFlow's blue with a single gold accent for the campaign. Close enough to the brand that it reads as ",[536,1850,1851],{},"them",", distinct enough that it reads as an ",[536,1854,1855],{},"occasion",". No purple, no sub-brand. Just BethelFlow in a good mood.",[36,1858,1860],{"id":1859},"the-photo-never-leaves-your-browser","The photo never leaves your browser",[16,1862,1863],{},"Here's the decision I'm still happy about. Early on, I didn't want to store anyone's uploads. Partly cost, mostly trust. Asking someone to hand you a photo of their pastor on day one is a big ask, and \"where does this go?\" is a fair question.",[16,1865,1866,1867,1870],{},"So at first, nothing went anywhere. The draft, your words and your photo, lived in the browser's own IndexedDB, survived a reload, and rendered the card locally. Download straight from the page. ",[536,1868,1869],{},"\"We're not storing your images\""," wasn't marketing. It was just literally where the code stood. The photo genuinely never left the device.",[16,1872,1873],{},[19,1874],{"alt":1875,"src":1876,"width":1706,"height":1769},"Step two: adding a photo, with the card preview now showing the portrait behind the quote.","\u002Fimages\u002Fblog\u002Fmy-pastor-once-said\u002Fcompose-photo.png",[16,1878,1879],{},"That worked beautifully right up until people wanted to do the one thing the whole campaign was about: send it to someone.",[36,1881,1883],{"id":1882},"the-url-is-not-a-database","The URL is not a database",[16,1885,1886,1887,1043,1890,1893,1894,1897],{},"A couple of friends, ",[1076,1888],{"handle":1889},"codiejay",[1076,1891],{"handle":1892},"Tolu-Mals",", are the reason this part of the story exists at all. They made their cards and immediately hit the wall the whole campaign was pointed at: they wanted to ",[536,1895,1896],{},"send"," them, and sharing wasn't really there yet. Their nudging is what turned \"download and keep\" into \"share and pass on,\" and, a few detours later, what put photos on Vercel Blob.",[16,1899,1900,1901,1904],{},"Because sharing breaks the no-storage stance. A shared link needs two things I didn't have: a page that lives at a URL, and an image a link preview can actually show. My first instinct was to cling to the principle and smuggle the entire card ",[536,1902,1903],{},"into the link",", encoding the name, quote, and year as a blob of base64 in the path, so the URL itself carried the card and I still stored nothing.",[16,1906,1907],{},"Two walls, fast. A phone photo is two to five megabytes; it cannot live in a URL, full stop, no matter how you encode it. And even the text-only version produced links about three hundred and twenty characters long, the kind of link that looks less like a keepsake and more like something a stranger DMs you before stealing your bank details.",[16,1909,1910,1911,1914,1915,1918],{},"I gave up gracefully. Photos moved to Vercel Blob, but on my terms: the upload only fires when you actually reach for ",[536,1912,1913],{},"share",", it's deduped by a hash of the file so refreshing never piles up copies, and a weekly job sweeps anything older than ninety days. The promise softened honestly, ",[536,1916,1917],{},"we don't store it until you choose to send it",", instead of breaking. And in return we got real shareable pages, copy-to-clipboard, and link previews with the actual card in them.",[16,1920,1921],{},[19,1922],{"alt":1923,"src":1924,"width":1706,"height":1769},"The result screen: \"Download and share,\" with the finished card and a row of share buttons.","\u002Fimages\u002Fblog\u002Fmy-pastor-once-said\u002Fshare-light.png",[36,1926,1928],{"id":1927},"the-preview-that-rendered-nothing","The preview that rendered nothing",[16,1930,1931],{},"The links worked. The previews didn't. A shared card came up blank where the image should be. I went hunting in the usual places first: the upload, the cache, my own markup. It took a while before it clicked that the culprit wasn't the card at all. It was satori.",[16,1933,1934,1935,1938],{},"The link-preview image is generated with ",[111,1936,1937],{},"next\u002Fog",", which is satori under the hood. My card had a scrim, the dark gradient that keeps white text legible over a bright photo, and I'd written it the way you write it in a browser: pinned to all four edges, no explicit size. Every browser understands that. Satori doesn't. It sized the scrim to zero by zero, painted nothing, and on any light-coloured photo the white quote simply dissolved into the background.",[16,1940,1941],{},"The fix was one of those humbling one-liners: give the overlay real dimensions. The renderer that isn't a browser needs to be told the things a browser would have inferred. Scrim painted, text back, previews readable.",[36,1943,1945],{"id":1944},"shorter-prettier-links","Shorter, prettier links",[16,1947,1948],{},"The base64 links still bothered me. A keepsake shouldn't look like a ransom note. So the payload came out of the URL entirely. Each shared card became a tiny record with a random eight-character id, and the link just points at that. Roughly three hundred and twenty characters down to about fifty.",[16,1950,1951],{},"The part I cared about was not breaking anyone who'd already shared. The resolver just looks at the length and picks a path. A short id gets looked up, an old long token still decodes inline like before.",[104,1953,1957],{"className":1954,"code":1955,"language":1956,"meta":109,"style":109},"language-ts shiki shiki-themes github-light github-dark","return param.length \u003C= 16 ? loadShareCard(param) : decodeCard(param)\n","ts",[111,1958,1959],{"__ignoreMap":109},[114,1960,1961,1964,1967,1970,1973,1976,1979,1982,1985,1988,1991],{"class":116,"line":117},[114,1962,1963],{"class":127},"return",[114,1965,1966],{"class":137}," param.",[114,1968,1969],{"class":144},"length",[114,1971,1972],{"class":127}," \u003C=",[114,1974,1975],{"class":144}," 16",[114,1977,1978],{"class":127}," ?",[114,1980,1981],{"class":212}," loadShareCard",[114,1983,1984],{"class":137},"(param) ",[114,1986,1987],{"class":127},":",[114,1989,1990],{"class":212}," decodeCard",[114,1992,1993],{"class":137},"(param)\n",[16,1995,1996],{},"Nobody's link died. Old and new both resolve to the same card. Migrations you can skip are the best kind.",[36,1998,2000],{"id":1999},"the-first-day-and-the-dip","The first day, and the dip",[16,2002,2003,2004,2007,2008,2011],{},"Launch day did better than I'd let myself hope. In a single day the page took ",[584,2005,2006],{},"168 views from 117 different people",", and over the run it made ",[584,2009,2010],{},"72 cards from 42 of them",". For a side campaign built around a feeling, that's a lot of people choosing to make something.",[16,2013,2014],{},"And then, as campaigns do, it breathed out. The day after launch it was 56 views, then 18 the day after, then single digits. A steady exhale over the week.",[16,2016,2017,2018,2021,2022,2025],{},"Some of that taper is just gravity. But some of it I did to myself. Midweek I shipped a security-headers change, closed my laptop, and went to bed pleased. The new Content-Security-Policy quietly broke image downloading for everyone: the browser refused to fetch an image the app had literally just made. PostHog is how I found out. ",[584,2019,2020],{},"42 failed shares",", almost all of them from a ",[536,2023,2024],{},"handful"," of people tapping a dead button over and over, exactly the way you jab an elevator button that isn't wired to anything.",[16,2027,2028,2029,2033],{},"That bug got its own post, because it deserved one. ",[83,2030,2032],{"href":2031},"\u002Fblog\u002Fthe-button-that-did-nothing","The button that did nothing"," is the full autopsy. The short version: I'd been building on the one device where everything worked, and instrumenting failure so faintly that only luck told me it was happening.",[36,2035,2037],{"id":2036},"most-of-them-were-on-a-phone","Most of them were on a phone",[16,2039,2040],{},"The same dashboard said something quieter and more important than any single bug. Most people arrived on a phone, and on a phone they finished at less than half the rate they did on a desktop. I had lovingly designed a three-step, side-by-side wizard for a screen most of my users weren't holding.",[16,2042,2043,2044,1043,2046,2049],{},"So the create flow got rebuilt for a thumb. The separate ",[536,2045,116],{},[536,2047,2048],{},"photo"," steps merged into one compose screen with the card preview pinned to the top, so the payoff is always in frame instead of two taps away. \"Take a photo\" wires straight to the camera. Desktop keeps its roomy stepped wizard. The layout branches on the viewport rather than pretending one shape fits both.",[16,2051,2052],{},"The rest was performance, because a beautiful page that stutters on a mid-range Android is not a beautiful page. The marquee, that scrolling wall of example cards, got a lighter animation that pauses on hover and stands still for anyone who's asked for reduced motion. The decorative images lazy-load so they stop fighting the first paint. And the card-capture engine changed under the hood after WebKit kept dropping the photo out of exports.",[16,2054,2055],{},[19,2056],{"alt":2057,"src":2058,"width":1706,"height":2059},"The scrolling wall of community cards, real lines people submitted.","\u002Fimages\u002Fblog\u002Fmy-pastor-once-said\u002Fmarquee.png",612,[36,2061,2063],{"id":2062},"one-card-two-themes","One card, two themes",[16,2065,2066],{},"A small thing I'm fond of: the card and the whole page render in light and dark off the same live preview, so it looks intentional whichever way your phone is set. Same card, same words, different room.",[16,2068,2069],{},[19,2070],{"alt":2071,"src":2072,"width":1706,"height":1769},"The result screen in dark mode, the card glowing against a near-black page.","\u002Fimages\u002Fblog\u002Fmy-pastor-once-said\u002Fshare-dark.png",[36,2074,957],{"id":956},[16,2076,2077],{},"Here's the whole shape of it, start to finish.",[16,2079,2080],{},[19,2081],{"alt":2082,"src":2083,"width":2084,"height":2085},"A showcase board of the finished product: the landing page, the three-step create flow, the light and dark cards, and the community wall.","\u002Fimages\u002Fblog\u002Fmy-pastor-once-said\u002Fshowcase.png",4800,9298,[16,2087,2088,2089,2092,2093,1776,2096,2092,2099,1776,2102,2092,2105,2108],{},"Looking back, the thread through every good decision was the same. I kept holding a principle, and users kept, gently, asking me to bend it. ",[536,2090,2091],{},"Don't store the photo"," met ",[536,2094,2095],{},"let me send this to my sister.",[536,2097,2098],{},"Keep it pure in the URL",[536,2100,2101],{},"a phone photo is five megabytes.",[536,2103,2104],{},"Ship the elegant wizard",[536,2106,2107],{},"I'm on a bus, on a phone, using one thumb."," The design got better every time I let the principle bend without letting it break: storing photos only at the moment of sharing, shortening links without stranding old ones, rebuilding the flow for the device people actually held.",[16,2110,2111,2112,2116],{},"It's evergreen, and it's still up. If your pastor had a line you never forgot, the one you can still hear in their voice, go ",[83,2113,2115],{"href":1789,"rel":2114},[87],"make a card",". Send it to someone who needs it today. We'd genuinely love to see the ones that stuck with you.",[968,2118,2119],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":109,"searchDepth":124,"depth":124,"links":2121},[2122,2123,2124,2125,2126,2127,2128,2129,2130,2131,2132],{"id":1761,"depth":154,"text":1762},{"id":1802,"depth":124,"text":1803},{"id":1828,"depth":124,"text":1829},{"id":1859,"depth":124,"text":1860},{"id":1882,"depth":124,"text":1883},{"id":1927,"depth":124,"text":1928},{"id":1944,"depth":124,"text":1945},{"id":1999,"depth":124,"text":2000},{"id":2036,"depth":124,"text":2037},{"id":2062,"depth":124,"text":2063},{"id":956,"depth":124,"text":957},"2026-08-09","The product-design story behind a BethelFlow campaign, a card generator for the lines our pastors never stopped saying. What we designed, the rough edges, the numbers, and where it landed.",{},"\u002Fblog\u002Fmy-pastor-once-said","9min",{"title":1756,"description":2134},"blog\u002Fmy-pastor-once-said","FkwpYtrAnlXg4h9Qpune8ah3nFD8aznmSkgXL3kNU8I",{"id":2142,"title":2032,"body":2143,"canonical":992,"date":2568,"description":2569,"draft":995,"duration":992,"extension":996,"lang":992,"meta":2570,"navigation":196,"path":2031,"place":992,"placeLink":992,"readingTime":2571,"seo":2572,"stem":2573,"__hash__":2574},"blog\u002Fblog\u002Fthe-button-that-did-nothing.md",{"type":8,"value":2144,"toc":2558},[2145,2149,2156,2159,2162,2166,2180,2187,2190,2194,2197,2215,2253,2256,2282,2285,2404,2414,2418,2429,2438,2450,2454,2460,2463,2467,2470,2478,2485,2521,2524,2531,2535,2538,2541,2545,2548,2555],[11,2146,2148],{"id":2147},"most-of-my-users-lived-on-a-phone-i-built-the-whole-thing-on-a-laptop-heres-what-that-cost-and-the-rule-i-keep-now","Most of my users lived on a phone. I built the whole thing on a laptop. Here's what that cost, and the rule I keep now.",[16,2150,2151],{},[19,2152],{"alt":2153,"src":2154,"fetchPriority":23,"loading":24,"width":1018,"height":2155},"A download button pressed over and over, each tap rippling out into nothing, with a counter reading times twenty.","\u002Fimages\u002Fblog\u002Fthe-button-that-did-nothing\u002Fcover.svg",520,[16,2157,2158],{},"You've met a dead button. The crosswalk one that isn't wired to anything. The elevator \"close door\" that's decoration. The one you press, and when nothing happens you press it again, harder, because surely it heard you the first time. There's a particular small humiliation in it, standing there jabbing a button while the world declines to respond.",[16,2160,2161],{},"I built one of those. Not for me. For most of the people who showed up.",[36,2163,2165],{"id":2164},"the-recording-i-couldnt-stop-watching","The recording I couldn't stop watching",[16,2167,2168,2169,2172,2173,2175,2176,2179],{},"We shipped a little thing on ",[83,2170,1798],{"href":1796,"rel":2171},[87]," called ",[536,2174,1756],{},". You type a line your pastor never stopped saying, add a photo, and it makes a shareable card. Simple, joyful, exactly the kind of thing that's supposed to just work. I wrote about ",[83,2177,2178],{"href":2136},"designing the whole campaign"," separately. This post is about the part that broke.",[16,2181,2182,2183,2186],{},"Then I opened a session replay one morning. An iPhone. Mobile Safari. Someone had made their card. It was right there on the screen, finished, theirs. Then they tapped ",[584,2184,2185],{},"Download",". Nothing. They tapped it again. Again. I counted later: twenty times over eight minutes. Then they tried WhatsApp twice, then they left. No card ever reached them.",[16,2188,2189],{},"I watched it the way you watch someone wave at a sliding door that isn't going to open. And the worst part is that on my machine, a desktop running Chrome, I had pressed that exact button a hundred times and it worked every single time. It worked in the only place I ever tested it.",[36,2191,2193],{"id":2192},"three-different-silences","Three different silences",[16,2195,2196],{},"When I finally pulled the button apart, it wasn't broken in one way. It was quietly broken in three, and each one was invisible from where I sat.",[16,2198,2199,2202,2203,2206,2207,2210,2211,2214],{},[584,2200,2201],{},"One: iOS doesn't honour the download trick."," The pattern every tutorial teaches is simple: make an anchor, set ",[111,2204,2205],{},"download",", click it in code. Safari on iOS simply ignores it. And my click landed ",[536,2208,2209],{},"after"," an ",[111,2212,2213],{},"await"," while the image rendered, so the user's tap was already spent by the time the download tried to fire.",[104,2216,2218],{"className":1954,"code":2217,"language":1956,"meta":109,"style":109},"const dataUrl = await renderCardDataUrl(cardRef.current) \u002F\u002F the tap is gone by here\nsaveDataUrl(dataUrl, filename)                           \u002F\u002F iOS: politely does nothing\n",[111,2219,2220,2242],{"__ignoreMap":109},[114,2221,2222,2224,2227,2230,2233,2236,2239],{"class":116,"line":117},[114,2223,128],{"class":127},[114,2225,2226],{"class":144}," dataUrl",[114,2228,2229],{"class":127}," =",[114,2231,2232],{"class":127}," await",[114,2234,2235],{"class":212}," renderCardDataUrl",[114,2237,2238],{"class":137},"(cardRef.current) ",[114,2240,2241],{"class":120},"\u002F\u002F the tap is gone by here\n",[114,2243,2244,2247,2250],{"class":116,"line":124},[114,2245,2246],{"class":212},"saveDataUrl",[114,2248,2249],{"class":137},"(dataUrl, filename)                           ",[114,2251,2252],{"class":120},"\u002F\u002F iOS: politely does nothing\n",[16,2254,2255],{},"On desktop this saves a file. On a phone it's a dead button. Same code, opposite outcome, and I only ever stood on the side where it worked.",[16,2257,2258,2261,2262,2265,2266,2269,2270,2273,2274,2277,2278,2281],{},[584,2259,2260],{},"Two: the failure underneath, that hit everyone."," Our security headers set ",[111,2263,2264],{},"connect-src 'self' https: wss:",". Reasonable. But building the image meant turning a data URL into a blob, and I'd done it the lazy way, ",[111,2267,2268],{},"fetch(dataUrl)",". ",[111,2271,2272],{},"fetch"," answers to ",[111,2275,2276],{},"connect-src",". So the browser blocked me from fetching ",[536,2279,2280],{},"my own image",", the call threw, and the file was never built. Not just on iOS. Everywhere. iOS just failed more loudly because it had no fallback to limp to.",[16,2283,2284],{},"The fix was to stop asking the network for something I already held in my hand, and decode the base64 myself:",[104,2286,2288],{"className":1954,"code":2287,"language":1956,"meta":109,"style":109},"\u002F\u002F was: const blob = await fetch(dataUrl).then(r => r.blob())  \u002F\u002F CSP blocks this\nconst [head, body] = dataUrl.split(\",\", 2)\nconst bytes = Uint8Array.from(atob(body), c => c.charCodeAt(0))\nconst blob = new Blob([bytes], { type: \"image\u002Fpng\" })\n",[111,2289,2290,2298,2336,2380],{"__ignoreMap":109},[114,2291,2292,2295],{"class":116,"line":117},[114,2293,2294],{"class":120},"\u002F\u002F was: const blob = await fetch(dataUrl).then(r => r.blob())",[114,2296,2297],{"class":120},"  \u002F\u002F CSP blocks this\n",[114,2299,2300,2302,2305,2308,2310,2313,2316,2318,2321,2324,2326,2329,2331,2333],{"class":116,"line":124},[114,2301,128],{"class":127},[114,2303,2304],{"class":137}," [",[114,2306,2307],{"class":144},"head",[114,2309,1085],{"class":137},[114,2311,2312],{"class":144},"body",[114,2314,2315],{"class":137},"] ",[114,2317,141],{"class":127},[114,2319,2320],{"class":137}," dataUrl.",[114,2322,2323],{"class":212},"split",[114,2325,299],{"class":137},[114,2327,2328],{"class":1321},"\",\"",[114,2330,1085],{"class":137},[114,2332,302],{"class":144},[114,2334,2335],{"class":137},")\n",[114,2337,2338,2340,2343,2345,2348,2351,2353,2356,2359,2363,2366,2369,2372,2374,2377],{"class":116,"line":154},[114,2339,128],{"class":127},[114,2341,2342],{"class":144}," bytes",[114,2344,2229],{"class":127},[114,2346,2347],{"class":137}," Uint8Array.",[114,2349,2350],{"class":212},"from",[114,2352,299],{"class":137},[114,2354,2355],{"class":212},"atob",[114,2357,2358],{"class":137},"(body), ",[114,2360,2362],{"class":2361},"s4XuR","c",[114,2364,2365],{"class":127}," =>",[114,2367,2368],{"class":137}," c.",[114,2370,2371],{"class":212},"charCodeAt",[114,2373,299],{"class":137},[114,2375,2376],{"class":144},"0",[114,2378,2379],{"class":137},"))\n",[114,2381,2382,2384,2387,2389,2392,2395,2398,2401],{"class":116,"line":174},[114,2383,128],{"class":127},[114,2385,2386],{"class":144}," blob",[114,2388,2229],{"class":127},[114,2390,2391],{"class":127}," new",[114,2393,2394],{"class":212}," Blob",[114,2396,2397],{"class":137},"([bytes], { type: ",[114,2399,2400],{"class":1321},"\"image\u002Fpng\"",[114,2402,2403],{"class":137}," })\n",[16,2405,2406,2409,2410,2413],{},[584,2407,2408],{},"Three: the button, hammered."," To feel snappy, I'd pre-warmed the share on hover. But on every failure it reset and tried again on the ",[536,2411,2412],{},"next"," pointer event. So a person poking a dead button didn't just get silence. They fired off eleven, fifteen failed requests into the dark, and not one of them made a sound I could hear.",[36,2415,2417],{"id":2416},"the-colour-that-never-came","The colour that never came",[16,2419,2420,2421,2424,2425,2428],{},"There was a quieter one still. On this card, the photo ",[536,2422,2423],{},"is"," the colour. Without it you get a flat gradient. iPhones shoot photos as HEIC, and an ",[111,2426,2427],{},"\u003Cimg>"," renders HEIC on Safari and almost nowhere else. So a card built and shared from a phone, opened by a friend on Chrome, lost its photo entirely. And the handler I'd written to be \"safe\" made sure no one ever knew:",[104,2430,2432],{"className":1193,"code":2431,"language":1195,"meta":109,"style":109},"\u003Cimg src={imageUrl} onError={(e) => { e.currentTarget.style.display = \"none\" }} \u002F>\n",[111,2433,2434],{"__ignoreMap":109},[114,2435,2436],{"class":116,"line":117},[114,2437,2431],{},[16,2439,2440,2441,2445,2446,2449],{},"Hide the broken image. No error, no log, just a colourless card where a face should be. I've written before about ",[83,2442,2444],{"href":2443},"\u002Fblog\u002Fquietest-bug","silence wearing the costume of safety",". Here it was again, in a different disguise. The fix was to stop trusting whatever the phone handed me and re-encode every upload to a plain JPEG the moment it's picked. And if it genuinely can't be decoded, to ",[536,2447,2448],{},"say so"," instead of swallowing it.",[36,2451,2453],{"id":2452},"the-number-i-hadnt-looked-at","The number I hadn't looked at",[16,2455,2456,2457],{},"Fixing the button felt like the work. It wasn't. The work was the number I'd never bothered to open: ",[584,2458,2459],{},"who was actually here.",[16,2461,2462],{},"On mobile, 148 people landed and 26 made a card. An 18% finish. On desktop, 42 landed and 17 made a card. Forty percent. Seventy-eight percent of everyone who came was on a phone, and on a phone they finished at less than half the rate they did on a desktop. I had poured the entire build into the 22% I could see. Not out of some decision, but because my machine happened to be one of them, and a laptop never once felt the dead button.",[36,2464,2466],{"id":2465},"building-where-your-users-live","Building where your users live",[16,2468,2469],{},"So we rebuilt the create flow for a thumb, not a mouse. One screen instead of three. The card previewing live as you type, so the payoff arrives while you're still working instead of two taps later. \"Take a photo\" wired straight to the camera.",[16,2471,2472],{},[19,2473],{"alt":2474,"src":2475,"width":2476,"height":2477},"The rebuilt mobile create flow: one screen, the card preview pinned to the top updating live as you fill the fields below.","\u002Fimages\u002Fblog\u002Fthe-button-that-did-nothing\u002Fcompose-mobile.png",1170,1870,[16,2479,2480,2481,2484],{},"And the part I'd change first if I could do it all again: we stopped ",[536,2482,2483],{},"pretending to be the platform",". Instead of hand-rolling a download the phone doesn't understand, we hand the finished card to the phone's own share sheet. The one button iOS actually knows how to press.",[104,2486,2488],{"className":1954,"code":2487,"language":1956,"meta":109,"style":109},"if (navigator.canShare?.({ files: [file] })) {\n  await navigator.share({ files: [file], title, text })\n}\n",[111,2489,2490,2504,2517],{"__ignoreMap":109},[114,2491,2492,2495,2498,2501],{"class":116,"line":117},[114,2493,2494],{"class":127},"if",[114,2496,2497],{"class":137}," (navigator.",[114,2499,2500],{"class":212},"canShare",[114,2502,2503],{"class":137},"?.({ files: [file] })) {\n",[114,2505,2506,2509,2512,2514],{"class":116,"line":124},[114,2507,2508],{"class":127},"  await",[114,2510,2511],{"class":137}," navigator.",[114,2513,1913],{"class":212},[114,2515,2516],{"class":137},"({ files: [file], title, text })\n",[114,2518,2519],{"class":116,"line":154},[114,2520,440],{"class":137},[16,2522,2523],{},"One line, and suddenly the button presses back. Save to Photos, WhatsApp, wherever. All the things the person was reaching for in the first place, through the door their phone already knew how to open.",[16,2525,2526],{},[19,2527],{"alt":2528,"src":2529,"width":2476,"height":2530},"The mobile result screen: the finished card with a full-width \"Share your card\" button that hands off to the phone's native share sheet.","\u002Fimages\u002Fblog\u002Fthe-button-that-did-nothing\u002Fshare-mobile.png",1690,[36,2532,2534],{"id":2533},"make-the-silence-loud","Make the silence loud",[16,2536,2537],{},"Then we did the unglamorous thing: we made failure audible. Every failed share now carries its reason with it, so the next time something goes quiet I don't have to get lucky with a replay to find out.",[16,2539,2540],{},"Because the real bug was never the anchor tag or the header. Those were an afternoon. The real bug was that a person stood there tapping twenty times and the only reason I ever knew was that I happened to scrub through a recording weeks later. A dead button is bad. A dead button you can't see is how you quietly lose three-quarters of the people who came.",[36,2542,2544],{"id":2543},"the-rule-i-keep","The rule I keep",[16,2546,2547],{},"Build on the device most of your users are actually holding, not the one you happen to own. And instrument every failure so it can't hide. The gap between a bug and a disaster is almost always just whether the button can tell you it's dead.",[16,2549,2550,2551,2554],{},"The card generator works on a phone now. If you've got a line your pastor never stopped saying, go make one at ",[83,2552,1756],{"href":1789,"rel":2553},[87],". Press the button. It should press back.",[968,2556,2557],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":109,"searchDepth":124,"depth":124,"links":2559},[2560,2561,2562,2563,2564,2565,2566,2567],{"id":2147,"depth":154,"text":2148},{"id":2164,"depth":124,"text":2165},{"id":2192,"depth":124,"text":2193},{"id":2416,"depth":124,"text":2417},{"id":2452,"depth":124,"text":2453},{"id":2465,"depth":124,"text":2466},{"id":2533,"depth":124,"text":2534},{"id":2543,"depth":124,"text":2544},"2026-08-07","Most of my users were on a phone, tapping a download button that silently did nothing. My desktop never once felt it. What a session recording taught me about building where your users actually live.",{},"7min",{"title":2032,"description":2569},"blog\u002Fthe-button-that-did-nothing","6-lQ-onD5eTE4vYyoYXwMP4a1Hm9mVw3mytfNo6cTTo",{"id":2576,"title":2577,"body":2578,"canonical":992,"date":3053,"description":3054,"draft":995,"duration":992,"extension":996,"lang":992,"meta":3055,"navigation":196,"path":3056,"place":992,"placeLink":992,"readingTime":992,"seo":3057,"stem":3058,"__hash__":3059},"blog\u002Fblog\u002Fcontext-is-everything.md","Context is everything",{"type":8,"value":2579,"toc":3045},[2580,2586,2599,2606,2609,2617,2625,2632,2638,2651,2731,2764,2791,2798,2802,2805,2812,2822,2832,2847,2858,2863,2870,2874,2877,2880,2891,2894,2904,2913,2949,2983,2993,2997,3004,3007,3010,3016,3020,3023,3030,3036,3039,3042],[16,2581,2582],{},[19,2583],{"alt":2584,"src":2585,"fetchPriority":23,"loading":24,"width":1018,"height":2155},"A conversation panel you've walked into halfway: the top lines cut off, the middle clear, the last line only half typed.","\u002Fimages\u002Fblog\u002Fcontext\u002Fcover.svg",[16,2587,2588,2589,2592,2593,2595,2596,449],{},"There's a specific kind of lost you feel walking into a conversation halfway through. People are laughing at something. Someone says \"yeah, but that's exactly what ",[536,2590,2591],{},"he"," did,\" and you nod along like you know who ",[536,2594,2591],{}," is. You don't. You've got the words but none of the wiring underneath them, and if you're not careful you'll repeat the half you caught as if it were the whole thing. That's the dangerous part. Not knowing nothing. Knowing ",[536,2597,2598],{},"half",[16,2600,2601,2602,2605],{},"We spend our whole lives quietly topping each other up. You mention a person and I already have years of them loaded. You say \"the usual\" and the waiter knows. Almost none of what makes a conversation work is ",[536,2603,2604],{},"in"," the conversation; it's the shared state both of you dragged in with you.",[16,2607,2608],{},"A language model has none of that. Every single call is a stranger walking in halfway through, and your entire job is to catch it up, in just the right number of words, before it says something confident and wrong.",[16,2610,2611,2612,2616],{},"For a good while I lived close to AI agents, a lot of it around a crypto Q&A agent called ",[83,2613,2615],{"href":1025,"rel":2614},[87],"AIDEN",". I haven't been in the thick of it for a bit now, but the lesson it drilled into me hasn't left. I've watched a model answer a question with total confidence and be completely, embarrassingly wrong, for one reason: it had been handed half the picture, with no way of knowing the other half existed. That's the part that stuck. The model isn't the hard part. Deciding what it gets to see is.",[36,2618,2620,2621,2624],{"id":2619},"the-art-of-deciding-what-not-to-say","The art of deciding what ",[536,2622,2623],{},"not"," to say",[16,2626,2627,2628,2631],{},"The early years of that codebase read like a diary of a fight against a tiny window. The model underneath had a context budget of about ",[584,2629,2630],{},"4,000 tokens",". Four thousand. And the questions people asked were things like \"what are the biggest gainers today,\" which means a crypto API payload with hundreds of coins and thousands of fields, enum lists containing every token id that has ever existed. None of it fit. Not close.",[16,2633,2634,2635,2637],{},"So what got built, long before big context windows made any of it look quaint, was an elaborate machine whose entire purpose was deciding what ",[584,2636,2623],{}," to send the model:",[2639,2640,2641,2648],"ul",{},[720,2642,2643,2644,2647],{},"There were ~30 API endpoints, too many to describe to a 4k model, so the endpoint ",[536,2645,2646],{},"descriptions"," got embedded and vector-searched to pull the top 10 candidates.",[720,2649,2650],{},"Ten was still too much. There's a loop, still in the codebase today, that encodes the candidate function definitions and just pops them off the end one at a time until they fit:",[104,2652,2654],{"className":1954,"code":2653,"language":1956,"meta":109,"style":109},"while (encode(functionsString).length > maxTokens && reducedEntries.length > 1) {\n  reducedEntries.pop()\n  functionsString = JSON.stringify(Object.fromEntries(reducedEntries))\n}\n",[111,2655,2656,2692,2703,2727],{"__ignoreMap":109},[114,2657,2658,2661,2663,2666,2669,2671,2674,2677,2680,2683,2685,2687,2690],{"class":116,"line":117},[114,2659,2660],{"class":127},"while",[114,2662,418],{"class":137},[114,2664,2665],{"class":212},"encode",[114,2667,2668],{"class":137},"(functionsString).",[114,2670,1969],{"class":144},[114,2672,2673],{"class":127}," >",[114,2675,2676],{"class":137}," maxTokens ",[114,2678,2679],{"class":127},"&&",[114,2681,2682],{"class":137}," reducedEntries.",[114,2684,1969],{"class":144},[114,2686,2673],{"class":127},[114,2688,2689],{"class":144}," 1",[114,2691,385],{"class":137},[114,2693,2694,2697,2700],{"class":116,"line":124},[114,2695,2696],{"class":137},"  reducedEntries.",[114,2698,2699],{"class":212},"pop",[114,2701,2702],{"class":137},"()\n",[114,2704,2705,2708,2710,2713,2715,2718,2721,2724],{"class":116,"line":154},[114,2706,2707],{"class":137},"  functionsString ",[114,2709,141],{"class":127},[114,2711,2712],{"class":144}," JSON",[114,2714,449],{"class":137},[114,2716,2717],{"class":212},"stringify",[114,2719,2720],{"class":137},"(Object.",[114,2722,2723],{"class":212},"fromEntries",[114,2725,2726],{"class":137},"(reducedEntries))\n",[114,2728,2729],{"class":116,"line":174},[114,2730,440],{"class":137},[2639,2732,2733,2744,2755,2758],{},[720,2734,2735,2736,2739,2740,2743],{},"Parameters like ",[111,2737,2738],{},"coin_ids"," have thousands of valid values, so those couldn't be inlined either: a second vector table ",[536,2741,2742],{},"of enum values",", searched against the query to return ~20 likely ones.",[720,2745,2746,2747,2750,2751,2754],{},"When an API response came back too big, it got filtered by ",[536,2748,2749],{},"another model call",", asking one to write a ",[111,2752,2753],{},"jq"," expression that projected the giant blob down to only the fields the question needed, then retrying by feeding it its own error.",[720,2756,2757],{},"Documents got chopped into 150-token chunks; keep the top 5, glue them together.",[720,2759,2760,2761],{},"Conversation history got truncated by hand: chop any assistant reply past 150 characters, counted by character, with the comment ",[536,2762,2763],{},"\"prevent information saturation.\"",[16,2765,2766,2767,2770,2771,2774,2775,2778,2779,2782,2783,2786,2787,2790],{},"The commit history is full of the desperation. ",[111,2768,2769],{},"MAX_TOKENS"," changed ",[584,2772,2773],{},"five times in a single day",": 4000, then 3500, then 4000, then 8000, then back to 4000. There's a one-liner that just ",[536,2776,2777],{},"\"increases max tokens to 3 times if it's not english,\""," because Korean and Chinese tokenize worse and there was no better idea at the time. There's a commit called ",[584,2780,2781],{},"\"avoid caching sorry answers,\""," because the system had been caching its own ",[536,2784,2785],{},"failures",". The README describes one of these subsystems and then admits, out loud, ",[536,2788,2789],{},"\"This is a big system as well 😅.\""," Twice.",[16,2792,2793,2794,2797],{},"When I got close to all of it, it was humbling to sit with. It felt less like engineering intelligence and more like being a very anxious travel agent for a genius who forgets everything the second you stop talking. But every one of those hacks is the same move underneath: ",[536,2795,2796],{},"don't dump, decide."," Nobody had a name for it yet. It just felt like janitorial work you did so the smart part could work at all.",[36,2799,2801],{"id":2800},"then-we-gave-it-a-team","Then we gave it a team",[16,2803,2804],{},"The rebuilds came as the models got bigger and smarter. You'd think that would make the whole problem evaporate.",[16,2806,2807,2808,2811],{},"It didn't. It just changed shape. In one of the newer versions the very first real crisis, only weeks in, was adding a token counter and model-based data filtering in the same week, because the fresh, powerful model was ",[536,2809,2810],{},"still"," drowning in raw API payloads. Same bottleneck. New house.",[16,2813,2814,2815,2818,2819],{},"But the answer this time wasn't a bigger pile of filters. It was decomposition. One overloaded API agent got split into five specialists (CoinGecko, DeBank, DeFiLlama, Etherscan, IQAI) with a top-level ",[584,2816,2817],{},"router"," that stopped answering anything itself and just classified each question and handed it to exactly one specialist. Single hop. Its instruction literally says ",[536,2820,2821],{},"\"Do NOT provide your own answer after transferring.\"",[16,2823,2824,2825,2828,2829,2831],{},"The point of that is context. A CoinGecko question now travels with ",[536,2826,2827],{},"only"," CoinGecko instructions and ",[536,2830,2827],{}," CoinGecko tools. It never has to carry DeBank's entire world in its head. Each agent gets a narrow, purpose-built slice of reality instead of one prompt straining to be everything at once.",[16,2833,2834,2835,2838,2839,2842,2843,2846],{},"And we got ruthless about history. There's a step that, before every model call on a specialist, throws the thread away and hands it ",[536,2836,2837],{},"only the current question",". The default number of past turns kept? ",[584,2840,2841],{},"One."," A tiny separate model rewrites your follow-up (\"what about ",[536,2844,2845],{},"its"," price?\") into a standalone question first, so the specialist never needs the backstory; it gets a question that already resolved \"it.\"",[16,2848,2849,2850,2853,2854,2857],{},"We even stopped dumping tool menus. Instead of loading dozens of endpoint schemas, an agent ",[536,2851,2852],{},"searches"," for the tool it needs, then writes code to call it. And the newest rebuilds lean into that hard: give a specialist a single tool, let the model write a little function that runs in a sandbox, do the fetch-filter-sort-slice ",[536,2855,2856],{},"inside"," that call, and hand back only the ten rows it asked for. The 250-row array it worked through never touches the model's context. There's a line in there I love:",[1618,2859,2860],{},[16,2861,2862],{},"Too large to hand back safely, return a truncated string so it can't overflow the consuming model's context window.",[16,2864,2865,2866,2869],{},"Years earlier the move was popping function definitions off a list until they fit. This is the grown-up version of the exact same instinct, and somewhere in the middle of watching all of it, I stopped seeing this as janitorial work and realized it ",[536,2867,2868],{},"was"," the job. The whole job.",[36,2871,2873],{"id":2872},"it-has-a-name-now","It has a name now",[16,2875,2876],{},"Somewhere around the middle of 2025, while I was buried in this stuff, watching agents keep choking on context they'd been handed badly, the rest of the field named the thing.",[16,2878,2879],{},"In June 2025, Shopify's Tobi Lütke put it like this:",[16,2881,2882,2888],{},[19,2883],{"alt":2884,"src":2885,"width":2886,"height":2887},"Tobi Lütke: “I really like the term ‘context engineering’ over prompt engineering… the art of providing all the context for the task to be plausibly solvable by the LLM.”","\u002Fimages\u002Fblog\u002Fcontext\u002Ftobi-context-engineering.png",1194,536,[536,2889,2890],{},"Tobi Lütke, June 19, 2025.",[16,2892,2893],{},"Days later, Andrej Karpathy amplified it into the definition everyone now quotes:",[16,2895,2896,2901],{},[19,2897],{"alt":2898,"src":2899,"width":2886,"height":2900},"Andrej Karpathy: “context engineering is the delicate art and science of filling the context window with just the right information for the next step.”","\u002Fimages\u002Fblog\u002Fcontext\u002Fkarpathy-context-engineering.png",1356,[536,2902,2903],{},"Andrej Karpathy, June 25, 2025. The line that renamed the discipline.",[1618,2905,2906],{},[16,2907,2908,2909,2912],{},"context engineering is the delicate art and science of filling the context window with just the right information for the next step. ",[114,2910,2911],{},"..."," Too little or of the wrong form and the LLM doesn't have the right context for optimal performance. Too much or too irrelevant and the LLM costs might go up and performance might come down.",[16,2914,2915,2916,2919,2920,2923,2924,2927,2928,2931,2932,2937,2938,2941,2942,2945,2946],{},"That last sentence is the part nobody warns you about. I used to think the whole problem was ",[536,2917,2918],{},"scarcity",", not enough room. So when the windows finally got huge, I assumed we'd won. The token budget in that codebase tells the real story: it went from around 4,000 to a full million, and then someone quietly dialed it back to 500,000, for performance. We'd been handed almost infinite room and ",[536,2921,2922],{},"chose"," to use less. Because more context isn't free context. Anthropic frames it as an ",[584,2925,2926],{},"attention budget",": like us, a model has limited working memory, and the more you cram in, the ",[536,2929,2930],{},"worse"," its recall gets. They call it \"context rot,\" where the answer is technically in the window but buried in the middle where nothing looks. The ",[83,2933,2936],{"href":2934,"rel":2935},"https:\u002F\u002Fwww.ai-engineer.me\u002Fcontext-is-everything\u002F",[87],"ai-engineer.me essay that shares this post's title"," puts it best: ",[536,2939,2940],{},"\"bottlenecks can't be eliminated, they can only be moved around.\""," Bigger windows didn't kill the bottleneck. They moved it from ",[536,2943,2944],{},"\"will it fit\""," to ",[536,2947,2948],{},"\"of everything that now fits, what actually deserves to be there.\"",[16,2950,2951,2952,2957,2958,2961,2962,2965,2966,2969,2970,2972,2973,2976,2977,2982],{},"When I read ",[83,2953,2956],{"href":2954,"rel":2955},"https:\u002F\u002Fwww.langchain.com\u002Fblog\u002Fcontext-engineering-for-agents",[87],"LangChain's four moves",", I laughed, because they're a tidy map of everything I'd watched us stumble into by feel. ",[584,2959,2960],{},"Write"," context out to somewhere durable, that's saving what an agent found so a later step can use it. ",[584,2963,2964],{},"Select"," the right pieces back in, that's the vector search. ",[584,2967,2968],{},"Compress"," what you keep, that's the ",[111,2971,2753],{}," filter and \"return a small summary.\" ",[584,2974,2975],{},"Isolate"," it across agents, that's the scoped specialists. There are now whole companies, like ",[83,2978,2981],{"href":2979,"rel":2980},"https:\u002F\u002Fsupermemory.ai\u002F",[87],"supermemory",", which bills itself as \"the memory layer for AI agents, a context engineering platform,\" selling the pillar we'd been hand-rolling since it was a 150-token chunk in a database table.",[16,2984,2985,2986,2989,2990],{},"Karpathy ended his post with a line I think about a lot: ",[536,2987,2988],{},"the term \"ChatGPT wrapper\" is tired and really, really wrong."," He's right. The model is one component. The thick, unglamorous layer around it, deciding what it sees, when, and in what shape, is most of the actual software. It's most of the actual ",[536,2991,2992],{},"work.",[36,2994,2996],{"id":2995},"back-to-the-humans","Back to the humans",[16,2998,2999,3000,3003],{},"Here's the thing that keeps getting me, though. We are ",[536,3001,3002],{},"terrible"," context engineers with each other, and we've had a lot longer to practise.",[16,3005,3006],{},"Think about how we brief people. We fire off the Slack message with none of the backstory and wonder why the reply misses. We say \"you know what I mean\" to someone who probably does not. We walk a new teammate up to a two-year-old system and drop them at the door. We do to each other exactly what a bad prompt does to a model: too little context, or the wrong shape, and then we're surprised by the confident wrong answer that comes back.",[16,3008,3009],{},"The uncomfortable mirror is that everything I learned bolting context onto a forgetful model is just... good communication, formalized. Rewrite the follow-up so it stands alone: that's not making someone reconstruct the whole thread from memory. Return a summary, not the raw arrays: that's respecting someone's attention budget instead of forwarding the 200-email chain. Give each specialist only their slice: that's not CC'ing forty people on something two of them own. Write it down somewhere durable: that's the doc that outlives the one person who remembers.",[16,3011,3012,3013],{},"We get away with being sloppy because humans are absurdly good at filling gaps. You lob half a sentence and repeated patterns, shared history, a lifted eyebrow do the rest. The model has none of that grace. It exposes, ruthlessly, how much of what we call \"communicating\" was actually just ",[536,3014,3015],{},"the other person quietly doing the context engineering for us.",[36,3017,3019],{"id":3018},"the-refactor","The refactor",[16,3021,3022],{},"Context is expensive. Assembling it, curating it, keeping it fresh, that's real cost, in tokens and in effort. But missing context is more expensive. It's the confident wrong answer, the rework, the argument that was really just two people running on different halves of the same conversation.",[16,3024,3025,3026,3029],{},"For years I thought I was solving a machine problem: squeeze the data through the tiny hole, then rank it once the hole got big. I was really learning the oldest problem there is: how do you get what's in your head ",[536,3027,3028],{},"plausibly, correctly"," into someone else's, using only the words you can afford to spend.",[16,3031,3032,3033],{},"The essay I keep quoting opens with a line that turned out to be the whole point: ",[536,3034,3035],{},"\"With LLMs, as with people, context is everything.\"",[16,3037,3038],{},"Every conversation with a model is a stranger walking in halfway through. Turns out most of my conversations with people are too. The real, defining skill, whether the other side is a language model or a person who deserves better than half the story, is catching them up before they nod along to something they only half understand.",[16,3040,3041],{},"So: say more than \"Hi.\" Say enough.",[968,3043,3044],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":109,"searchDepth":124,"depth":124,"links":3046},[3047,3049,3050,3051,3052],{"id":2619,"depth":124,"text":3048},"The art of deciding what not to say",{"id":2800,"depth":124,"text":2801},{"id":2872,"depth":124,"text":2873},{"id":2995,"depth":124,"text":2996},{"id":3018,"depth":124,"text":3019},"2026-07-18","What years of building AI agents taught me about context engineering, and how much of it turned out to be the same thing as briefing another human well.",{},"\u002Fblog\u002Fcontext-is-everything",{"title":2577,"description":3054},"blog\u002Fcontext-is-everything","6gQ2DAOU4a8428-7xnvYjce9pP7fTljyxYFTjqeYjPQ",{"id":3061,"title":3062,"body":3063,"canonical":992,"date":3429,"description":3430,"draft":995,"duration":992,"extension":996,"lang":992,"meta":3431,"navigation":196,"path":2443,"place":992,"placeLink":992,"readingTime":3432,"seo":3433,"stem":3434,"__hash__":3435},"blog\u002Fblog\u002Fquietest-bug.md","The quietest kind of bug",{"type":8,"value":3064,"toc":3421},[3065,3069,3072,3075,3078,3081,3084,3088,3099,3102,3113,3116,3119,3128,3151,3157,3167,3171,3177,3183,3186,3190,3197,3220,3235,3249,3255,3258,3267,3270,3274,3281,3284,3337,3340,3401,3404,3407,3409,3415,3418],[11,3066,3068],{"id":3067},"why-optional-chaining-hides-the-failure-you-most-need-to-see-and-the-rule-i-keep-for-it","Why optional chaining hides the failure you most need to see, and the rule I keep for it.",[3070,3071],"quiet-cover",{},[16,3073,3074],{},"You know the little light on your dashboard. The orange one.",[16,3076,3077],{},"It flicks on one morning and you tell yourself you'll deal with it later. The car still drives. Nothing feels different. So you keep going, day after day, the light glowing quietly in the corner of your eye until you stop seeing it at all. Then one evening, on the highway, late for something that actually matters, the engine gives out. The light had been trying to tell you for weeks. You'd just trained yourself not to listen.",[16,3079,3080],{},"We do this everywhere, don't we? The strange noise in the house you slowly tune out. The \"we should talk\" text you leave on read. The friend who says \"I'm fine\" so smoothly that you let yourself believe them. None of it is loud. All of it is waiting. The problem never actually left when the signal went quiet. It just picked its moment.",[16,3082,3083],{},"Code has an operator for exactly this. For years, it was my favourite one.",[36,3085,3087],{"id":3086},"the-code-that-never-speaks-up","The code that never speaks up",[16,3089,3090,3091,3094,3095,3098],{},"Optional chaining. ",[111,3092,3093],{},"?.",". You reach into an object, and if something along the way is missing, it politely returns ",[111,3096,3097],{},"undefined"," instead of throwing. No crash. No noise. It just keeps going.",[16,3100,3101],{},"That sounds like a gift until it costs you. Here is what it cost me.",[16,3103,3104,3105,3108,3109,3112],{},"On the church records page in ",[83,3106,1798],{"href":1796,"rel":3107},[87],", the names went blank. Not everywhere, and not all at once, but rows that should have read \"The Adebayo family, Ushering department\" were quietly showing a dash. ",[111,3110,3111],{},"—",". As if the record had no family and no department at all. No error. Nothing in the console. The data was sitting right there in the API response the whole time. The screen just refused to show it.",[16,3114,3115],{},"A user found it before I did. That part still stings.",[16,3117,3118],{},"Here is the line that did it, and I'll be honest with you: I wrote it on autopilot.",[104,3120,3122],{"className":1193,"code":3121,"language":1195,"meta":109,"style":109},"value={r.familyId ? (family?.familyName ?? \"—\") : null}\n",[111,3123,3124],{"__ignoreMap":109},[114,3125,3126],{"class":116,"line":117},[114,3127,3121],{},[16,3129,3130,3131,3134,3135,3137,3138,3140,3141,3144,3145,1085,3147,3150],{},"Read it slower than I did. ",[111,3132,3133],{},"family"," is a lookup object I fetched separately on the client. When it hadn't loaded yet, or the lookup simply missed, ",[111,3136,3133],{}," was ",[111,3139,3097],{},". So ",[111,3142,3143],{},"family?.familyName"," short-circuited to ",[111,3146,3097],{},[111,3148,3149],{},"?? \"—\""," caught it, and the row cheerfully rendered a dash. Optional chaining did exactly what it promised. It did not throw. It also never once mentioned that the whole premise of the line, \"I have this family object,\" was false.",[16,3152,3153,3154,3156],{},"And here's the part I have to own. I didn't reach for ",[111,3155,3093],{}," because I'd thought carefully about what should happen when a family is missing. I reached for it because the red squiggle went away. It felt like handling the case. It was the \"yeah, all good\" of code. It was silence wearing the costume of safety.",[16,3158,3159,3160,3163,3164,3166],{},"That's the take, and I'll say it plainly: ",[584,3161,3162],{},"optional chaining is a bug-swallowing machine."," It takes a loud, early failure and turns it into a quiet ",[111,3165,3097],{}," that either sits on the screen as a blank, or travels three functions downstream and finally explodes as \"cannot read x of undefined\" somewhere that has nothing to do with where it actually broke.",[36,3168,3170],{"id":3169},"guarding-harder-just-moves-the-silence","Guarding harder just moves the silence",[16,3172,3173,3174,3176],{},"When the names went blank, my first instinct was the wrong one. Guard harder. Sprinkle a few more ",[111,3175,3093],{}," upstream so nothing could possibly throw.",[16,3178,3179,3180,3182],{},"But think about what that actually does. It doesn't fix the bug. It moves the silence. Every ",[111,3181,3093],{}," you add is one more place that will show nothing instead of telling you something broke. You can optional-chain an entire file and never once be told what's missing. That isn't a safe codebase. It's a codebase that has quietly agreed never to alarm you. 😅",[16,3184,3185],{},"That isn't a fix. It's a strip of tape over the warning light, so now the dashboard looks calm too. It's the friend who only learned to say \"I'm fine\" more convincingly.",[36,3187,3189],{"id":3188},"am-i-saying-never-use-it-no","Am I saying never use it? No",[16,3191,3192,3193,3196],{},"I use optional chaining every single day. For data that is ",[536,3194,3195],{},"genuinely"," optional, reaching one level in is exactly what the operator is for:",[104,3198,3200],{"className":1954,"code":3199,"language":1956,"meta":109,"style":109},"const avatar = user.profile?.avatarUrl ?? fallback\n",[111,3201,3202],{"__ignoreMap":109},[114,3203,3204,3206,3209,3211,3214,3217],{"class":116,"line":117},[114,3205,128],{"class":127},[114,3207,3208],{"class":144}," avatar",[114,3210,2229],{"class":127},[114,3212,3213],{"class":137}," user.profile?.avatarUrl ",[114,3215,3216],{"class":127},"??",[114,3218,3219],{"class":137}," fallback\n",[16,3221,3222,3223,3226,3227,3230,3231,3234],{},"That's honest code. ",[111,3224,3225],{},"profile"," is allowed to be absent, and I decided right here, in one glance, what happens when it is. The absence is ",[536,3228,3229],{},"expected",", and it's ",[536,3232,3233],{},"handled",". That is the whole test.",[16,3236,3237,3238,1776,3241,3244,3245,3248],{},"My rule is a length limit. ",[584,3239,3240],{},"Three links is the ceiling.",[111,3242,3243],{},"a?.b?.c"," and I can still hold in my head which of those can be null and why. ",[111,3246,3247],{},"a?.b?.c?.d?.e"," and I have stopped reasoning entirely. That chain isn't safety, it's a nervous tic, a quiet confession that I don't know the shape of my own data and I'd like the language to paper over it for me.",[16,3250,3251,3252,3254],{},"Granted, three is a heuristic, not a law. Some days it's two. The number matters less than the honesty it forces: if the chain is long enough that you can't say out loud which link is allowed to be missing, the answer is almost never one more ",[111,3253,3093],{},". It's that you're reaching into the wrong object.",[16,3256,3257],{},"That was the real story in BethelFlow. The fix wasn't to guard the lookup harder. It was to stop chaining into a maybe-missing client lookup and put the name on the record itself, where it belonged:",[104,3259,3261],{"className":1193,"code":3260,"language":1195,"meta":109,"style":109},"value={r.familyId ? (r.familyName ?? \"—\") : null}\n",[111,3262,3263],{"__ignoreMap":109},[114,3264,3265],{"class":116,"line":117},[114,3266,3260],{},[16,3268,3269],{},"No lookup. No chain. Nothing left to go quietly missing.",[36,3271,3273],{"id":3272},"early-returns-say-the-quiet-part-out-loud","Early returns say the quiet part out loud",[16,3275,3276,3277,3280],{},"Before I reach for optional chaining now, I reach for a guard clause. I decide what the absence ",[536,3278,3279],{},"means",", at the point where it happens, while I still have the context to decide it well.",[16,3282,3283],{},"So instead of threading a maybe-null value through a whole function and hoping:",[104,3285,3287],{"className":1954,"code":3286,"language":1956,"meta":109,"style":109},"function greeting(user) {\n  return `Hi, ${user?.profile?.contact?.firstName ?? \"there\"}`\n}\n",[111,3288,3289,3304,3333],{"__ignoreMap":109},[114,3290,3291,3294,3297,3299,3302],{"class":116,"line":117},[114,3292,3293],{"class":127},"function",[114,3295,3296],{"class":212}," greeting",[114,3298,299],{"class":137},[114,3300,3301],{"class":2361},"user",[114,3303,385],{"class":137},[114,3305,3306,3308,3311,3313,3315,3317,3319,3322,3324,3327,3330],{"class":116,"line":124},[114,3307,415],{"class":127},[114,3309,3310],{"class":1321}," `Hi, ${",[114,3312,3301],{"class":137},[114,3314,3093],{"class":1321},[114,3316,3225],{"class":137},[114,3318,3093],{"class":1321},[114,3320,3321],{"class":137},"contact",[114,3323,3093],{"class":1321},[114,3325,3326],{"class":137},"firstName",[114,3328,3329],{"class":127}," ??",[114,3331,3332],{"class":1321}," \"there\"}`\n",[114,3334,3335],{"class":116,"line":154},[114,3336,440],{"class":137},[16,3338,3339],{},"I handle it at the door:",[104,3341,3343],{"className":1954,"code":3342,"language":1956,"meta":109,"style":109},"function greeting(user) {\n  if (!user.profile) return null\n  return `Hi, ${user.profile.contact.firstName}`\n}\n",[111,3344,3345,3357,3374,3397],{"__ignoreMap":109},[114,3346,3347,3349,3351,3353,3355],{"class":116,"line":117},[114,3348,3293],{"class":127},[114,3350,3296],{"class":212},[114,3352,299],{"class":137},[114,3354,3301],{"class":2361},[114,3356,385],{"class":137},[114,3358,3359,3361,3363,3366,3369,3371],{"class":116,"line":124},[114,3360,373],{"class":127},[114,3362,418],{"class":137},[114,3364,3365],{"class":127},"!",[114,3367,3368],{"class":137},"user.profile) ",[114,3370,1963],{"class":127},[114,3372,3373],{"class":144}," null\n",[114,3375,3376,3378,3380,3382,3384,3386,3388,3390,3392,3394],{"class":116,"line":154},[114,3377,415],{"class":127},[114,3379,3310],{"class":1321},[114,3381,3301],{"class":137},[114,3383,449],{"class":1321},[114,3385,3225],{"class":137},[114,3387,449],{"class":1321},[114,3389,3321],{"class":137},[114,3391,449],{"class":1321},[114,3393,3326],{"class":137},[114,3395,3396],{"class":1321},"}`\n",[114,3398,3399],{"class":116,"line":174},[114,3400,440],{"class":137},[16,3402,3403],{},"Yes, it's more typing. That's the honest cost, and I'll pay it every time, because of what it buys. The null case becomes a decision I actually made instead of a default I fell into. The rest of the function runs on known-good data, so the happy path stays flat and easy to read. And the next person, who is usually a tired future version of me, can read the top of the function and know exactly what it needs to work.",[16,3405,3406],{},"That last one is the real prize. A guard clause is how a function says its preconditions out loud, and that shared understanding is what lets code compose without everyone holding the whole call graph in their head. Optional chaining hides the precondition. An early return speaks it.",[36,3408,2544],{"id":2543},[16,3410,3411,3412,3414],{},"Reach for ",[111,3413,3093],{}," when the absence is expected and you've handled it, and keep the chain to about three. Reach for a guard when the absence would mean something is wrong, and handle it right there, while you still know why.",[16,3416,3417],{},"Because the kindest thing a piece of code can do is the same thing that little orange light was trying to do all along. When something is wrong, say so early, say it out loud, and say it close to where it happened, while listening is still cheap. Silence is not safety. Ask the user who spent a week staring at a dash where their family's name should have been.",[968,3419,3420],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":109,"searchDepth":124,"depth":124,"links":3422},[3423,3424,3425,3426,3427,3428],{"id":3067,"depth":154,"text":3068},{"id":3086,"depth":124,"text":3087},{"id":3169,"depth":124,"text":3170},{"id":3188,"depth":124,"text":3189},{"id":3272,"depth":124,"text":3273},{"id":2543,"depth":124,"text":2544},"2026-07-16","Optional chaining hides the failure you most need to see. A user found a bug in BethelFlow before I did because of it. My honest take, and the rule I keep.",{},"6min",{"title":3062,"description":3430},"blog\u002Fquietest-bug","LscYygaTC6HiOiZUjesPqjZzrHW5o5DrNmsQEJDLRWE",{"id":3437,"title":3438,"body":3439,"canonical":4567,"date":4568,"description":4569,"draft":995,"duration":992,"extension":996,"lang":992,"meta":4570,"navigation":196,"path":4571,"place":992,"placeLink":992,"readingTime":992,"seo":4572,"stem":4573,"__hash__":4574},"blog\u002Fblog\u002Fbeing-heard.md","Being Heard Isn't Just About Speaking — It's About Understanding",{"type":8,"value":3440,"toc":4548},[3441,3445,3450,3453,3464,3467,3470,3473,3476,3492,3496,3499,3505,3509,3516,3531,3535,3538,3549,3552,3556,3559,3562,3578,3581,3588,3592,3598,3604,3608,3611,3618,3653,3657,3660,3667,3750,3754,3757,3764,3867,3874,3956,3960,3963,4044,4048,4051,4058,4261,4265,4268,4402,4406,4409,4414,4475,4480,4513,4517,4524,4532,4536,4539,4542,4545],[11,3442,3444],{"id":3443},"from-human-connection-to-multilingual-ux-using-nextjs-and-next-intl-to-build-user-aware-language-ready-web-experiences","From human connection to multilingual UX — using Next.js and next-intl to build user-aware, language-ready web experiences.",[16,3446,3447],{},[19,3448],{"alt":109,"fetchPriority":23,"loading":24,"src":3449},"https:\u002F\u002Fcdn-images-1.medium.com\u002Fmax\u002F730\u002F1*MlejWnprLa0VZpKGNdt9vg.png",[16,3451,3452],{},"I've always loved to be heard — not just in the sense of speaking, but in a way that makes me feel understood. We all do. It's human nature. But more often than not, I've found myself in conversations where I wasn't truly listened to. I was acknowledged, but not understood.",[16,3454,3455,3456,3459,3460,3463],{},"There was a time when I kept explaining myself over and over, hoping the other person would get it. But they were listening through their own filter — hearing what they wanted, not what I was actually saying. And I realized something: Most of us communicate the way ",[536,3457,3458],{},"we"," want to be heard, instead of listening the way ",[536,3461,3462],{},"someone else"," wants to be understood.",[16,3465,3466],{},"But what if we chose to listen differently? What if, instead of assuming, we asked? What if we designed experiences that didn't just express what we wanted to say, but adapted to the way others understood best?",[16,3468,3469],{},"Just like in personal conversations, the way we design digital experiences determines whether users feel understood. Imagine visiting a website that doesn't support your language. You might understand bits and pieces, but the experience feels foreign, even alienating.",[16,3471,3472],{},"So, what does it really mean to listen? It's more than just acknowledging the presence of another — it's about making them feel at home, like they belong. In the digital world, listening means adapting to the user's needs, not expecting them to adapt to us.",[16,3474,3475],{},"Think about it: When you walk into a space where everything is unfamiliar — the language, the signs, the way things work — it takes extra effort just to navigate. You feel like an outsider. But when that same space acknowledges your language, your culture, and your way of thinking, suddenly, it feels welcoming. It feels like it was made for you.",[16,3477,3478,3479,3484,3485,449],{},"That's why ",[83,3480,3483],{"href":3481,"rel":3482},"https:\u002F\u002Fwww.techtarget.com\u002Fwhatis\u002Fdefinition\u002Finternationalization-I18N",[87],"internationalization (i18n)"," is so powerful. It's not just about translating words — it's about creating an experience that listens to the user, understands them, and speaks to them in a way that feels natural. And for Next.js developers, one of the best ways to achieve this is through ",[83,3486,3489],{"href":3487,"rel":3488},"https:\u002F\u002Fnext-intl.dev\u002Fdocs\u002Fgetting-started",[87],[584,3490,3491],{},"next-intl",[36,3493,3495],{"id":3494},"how-do-we-start-listening","How Do We Start Listening?",[16,3497,3498],{},"Just like in any conversation, there are different ways to listen. Sometimes, we listen through direct engagement — asking questions, responding in real time. Other times, we listen passively — observing patterns, understanding behavior, and adapting accordingly.",[16,3500,3501,3502,3504],{},"With ",[584,3503,3491],{},", we have similar choices when it comes to internationalization. We can structure our applications in a way that prioritizes adaptability, ensuring users feel like the experience was crafted for them. In Next.js, this comes down to two key approaches:",[11,3506,3508],{"id":3507},"with-i18n-routing-letting-the-url-speak","With i18n Routing → Letting the URL Speak",[16,3510,3511,3512,3515],{},"This approach integrates the language directly into the app's navigation. Just like entering a space where the signs immediately tell you, ",[536,3513,3514],{},"\"We speak your language,\""," this method ensures users instantly see content in their preferred language.",[2639,3517,3518,3525,3528],{},[720,3519,3520,3521,3524],{},"Uses a top-level ",[114,3522,3523],{},"locale"," segment for prefixed pathnames, such as \u002Fen\u002Fabout or \u002Fes\u002Fcontact.",[720,3526,3527],{},"Supports domain-based routing, like en.example.com\u002Fabout for region-specific experiences.",[720,3529,3530],{},"Ideal for applications that need clear and structured multilingual navigation.",[11,3532,3534],{"id":3533},"without-i18n-routing-listening-to-user-preferences","Without i18n Routing → Listening to User Preferences",[16,3536,3537],{},"Not every conversation starts with words; sometimes, we listen by observing. This approach allows applications to detect and adapt to a user's language preference dynamically.",[2639,3539,3540,3543,3546],{},[720,3541,3542],{},"Useful for apps that rely on user settings to determine language.",[720,3544,3545],{},"Works well for single-language applications that may need flexibility in the future.",[720,3547,3548],{},"Keeps URLs clean and language-independent, shifting the focus to user choice rather than URL structure.",[16,3550,3551],{},"Both methods have their place, just like different ways of listening in conversation. The key is to choose the one that best fits the needs of your users — because in the end, it's not just about what we build, but how we make people feel understood.",[36,3553,3555],{"id":3554},"getting-started-learning-to-listen-through-code","Getting Started: Learning to Listen Through Code",[16,3557,3558],{},"Just like in any meaningful conversation, listening starts with intention. In the world of applications, that intention shows up when we choose to support multiple languages — when we decide that users from anywhere should feel like the experience was designed with them in mind.",[16,3560,3561],{},"With Next.js and next-intl, that journey begins simply:",[104,3563,3565],{"className":1309,"code":3564,"language":1311,"meta":109,"style":109},"npm install next-intl\n",[111,3566,3567],{"__ignoreMap":109},[114,3568,3569,3572,3575],{"class":116,"line":117},[114,3570,3571],{"class":212},"npm",[114,3573,3574],{"class":1321}," install",[114,3576,3577],{"class":1321}," next-intl\n",[16,3579,3580],{},"But just like human conversations, how we listen matters just as much as what we hear. Next-intl gives us two ways to approach internationalization: one that makes language visible, and another that quietly adapts based on the user's preferences.",[16,3582,3583,3584,3587],{},"Let's begin with the first: ",[584,3585,3586],{},"i18n Routing"," — a way to let language speak through the URL itself.",[36,3589,3591],{"id":3590},"with-i18n-routing-making-language-part-of-the-experience","With i18n Routing: Making Language Part of the Experience",[16,3593,3594,3595],{},"When users land on a page and immediately see content in their language — like \u002Fen\u002Fabout or \u002Fes\u002Fcontact—they know: ",[536,3596,3597],{},"\"This space speaks to me.\"",[16,3599,3600,3601,3603],{},"This approach brings language into the very fabric of navigation, using a top-level ",[114,3602,3523],{}," segment in your Next.js routes. Setting it up is easier than it sounds.",[11,3605,3607],{"id":3606},"step-1-define-your-messages","Step 1: Define Your Messages",[16,3609,3610],{},"Each language gets its own JSON file. Here's a simple example:",[16,3612,3613],{},[536,3614,3615],{},[584,3616,3617],{},"messages\u002Fen.json:",[104,3619,3623],{"className":3620,"code":3621,"language":3622,"meta":109,"style":109},"language-json shiki shiki-themes github-light github-dark","{\n  \"HomePage\": {\n    \"title\": \"Hello world!\",\n    \"about\": \"Go to the about page\"\n  }\n}\n","json",[111,3624,3625,3630,3635,3640,3645,3649],{"__ignoreMap":109},[114,3626,3627],{"class":116,"line":117},[114,3628,3629],{},"{\n",[114,3631,3632],{"class":116,"line":124},[114,3633,3634],{},"  \"HomePage\": {\n",[114,3636,3637],{"class":116,"line":154},[114,3638,3639],{},"    \"title\": \"Hello world!\",\n",[114,3641,3642],{"class":116,"line":174},[114,3643,3644],{},"    \"about\": \"Go to the about page\"\n",[114,3646,3647],{"class":116,"line":193},[114,3648,404],{},[114,3650,3651],{"class":116,"line":200},[114,3652,440],{},[11,3654,3656],{"id":3655},"step-2-configure-nextjs-for-locales","Step 2: Configure Next.js for Locales",[16,3658,3659],{},"With next-intl, we wrap our config to enable locale-based routing.",[16,3661,3662],{},[536,3663,3664],{},[584,3665,3666],{},"next.config.ts",[104,3668,3670],{"className":1954,"code":3669,"language":1956,"meta":109,"style":109},"import { NextConfig } from 'next';\nimport createNextIntlPlugin from 'next-intl\u002Fplugin';\n\nconst nextConfig: NextConfig = {};\nconst withNextIntl = createNextIntlPlugin();\nexport default withNextIntl(nextConfig);\n",[111,3671,3672,3687,3701,3705,3722,3737],{"__ignoreMap":109},[114,3673,3674,3677,3680,3682,3685],{"class":116,"line":117},[114,3675,3676],{"class":127},"import",[114,3678,3679],{"class":137}," { NextConfig } ",[114,3681,2350],{"class":127},[114,3683,3684],{"class":1321}," 'next'",[114,3686,171],{"class":137},[114,3688,3689,3691,3694,3696,3699],{"class":116,"line":124},[114,3690,3676],{"class":127},[114,3692,3693],{"class":137}," createNextIntlPlugin ",[114,3695,2350],{"class":127},[114,3697,3698],{"class":1321}," 'next-intl\u002Fplugin'",[114,3700,171],{"class":137},[114,3702,3703],{"class":116,"line":154},[114,3704,197],{"emptyLinePlaceholder":196},[114,3706,3707,3709,3712,3714,3717,3719],{"class":116,"line":174},[114,3708,128],{"class":127},[114,3710,3711],{"class":144}," nextConfig",[114,3713,1987],{"class":127},[114,3715,3716],{"class":212}," NextConfig",[114,3718,2229],{"class":127},[114,3720,3721],{"class":137}," {};\n",[114,3723,3724,3726,3729,3731,3734],{"class":116,"line":193},[114,3725,128],{"class":127},[114,3727,3728],{"class":144}," withNextIntl",[114,3730,2229],{"class":127},[114,3732,3733],{"class":212}," createNextIntlPlugin",[114,3735,3736],{"class":137},"();\n",[114,3738,3739,3742,3745,3747],{"class":116,"line":200},[114,3740,3741],{"class":127},"export",[114,3743,3744],{"class":127}," default",[114,3746,3728],{"class":212},[114,3748,3749],{"class":137},"(nextConfig);\n",[11,3751,3753],{"id":3752},"step-3-define-routing-and-navigation-behavior","Step 3: Define Routing and Navigation Behavior",[16,3755,3756],{},"Tell your app what languages it understands, and what to fall back on if it doesn't.",[16,3758,3759],{},[536,3760,3761],{},[584,3762,3763],{},"src\u002Fi18n\u002Frouting.ts",[104,3765,3767],{"className":1954,"code":3766,"language":1956,"meta":109,"style":109},"import { defineRouting } from 'next-intl\u002Frouting';\nimport { createNavigation } from 'next-intl\u002Fnavigation';\n\nexport const routing = defineRouting({\n  \u002F\u002F list of all locales that are supported\n  locales: ['en', 'es', 'kr'],\n\n  \u002F\u002F Used when no locale matches, fallback to default\n  defaultLocale: 'en'\n});\n",[111,3768,3769,3783,3797,3801,3819,3824,3845,3849,3854,3862],{"__ignoreMap":109},[114,3770,3771,3773,3776,3778,3781],{"class":116,"line":117},[114,3772,3676],{"class":127},[114,3774,3775],{"class":137}," { defineRouting } ",[114,3777,2350],{"class":127},[114,3779,3780],{"class":1321}," 'next-intl\u002Frouting'",[114,3782,171],{"class":137},[114,3784,3785,3787,3790,3792,3795],{"class":116,"line":124},[114,3786,3676],{"class":127},[114,3788,3789],{"class":137}," { createNavigation } ",[114,3791,2350],{"class":127},[114,3793,3794],{"class":1321}," 'next-intl\u002Fnavigation'",[114,3796,171],{"class":137},[114,3798,3799],{"class":116,"line":154},[114,3800,197],{"emptyLinePlaceholder":196},[114,3802,3803,3805,3808,3811,3813,3816],{"class":116,"line":174},[114,3804,3741],{"class":127},[114,3806,3807],{"class":127}," const",[114,3809,3810],{"class":144}," routing",[114,3812,2229],{"class":127},[114,3814,3815],{"class":212}," defineRouting",[114,3817,3818],{"class":137},"({\n",[114,3820,3821],{"class":116,"line":193},[114,3822,3823],{"class":120},"  \u002F\u002F list of all locales that are supported\n",[114,3825,3826,3829,3832,3834,3837,3839,3842],{"class":116,"line":200},[114,3827,3828],{"class":137},"  locales: [",[114,3830,3831],{"class":1321},"'en'",[114,3833,1085],{"class":137},[114,3835,3836],{"class":1321},"'es'",[114,3838,1085],{"class":137},[114,3840,3841],{"class":1321},"'kr'",[114,3843,3844],{"class":137},"],\n",[114,3846,3847],{"class":116,"line":206},[114,3848,197],{"emptyLinePlaceholder":196},[114,3850,3851],{"class":116,"line":219},[114,3852,3853],{"class":120},"  \u002F\u002F Used when no locale matches, fallback to default\n",[114,3855,3856,3859],{"class":116,"line":228},[114,3857,3858],{"class":137},"  defaultLocale: ",[114,3860,3861],{"class":1321},"'en'\n",[114,3863,3864],{"class":116,"line":236},[114,3865,3866],{"class":137},"});\n",[16,3868,3869],{},[536,3870,3871],{},[584,3872,3873],{},"src\u002Fi18n\u002Fnavigation.ts",[104,3875,3877],{"className":1954,"code":3876,"language":1956,"meta":109,"style":109},"import {createNavigation} from 'next-intl\u002Fnavigation';\nimport {routing} from '.\u002Frouting';\n\nexport const {Link, redirect, usePathname, useRouter, getPathname} =\n  createNavigation(routing);\n",[111,3878,3879,3892,3906,3910,3948],{"__ignoreMap":109},[114,3880,3881,3883,3886,3888,3890],{"class":116,"line":117},[114,3882,3676],{"class":127},[114,3884,3885],{"class":137}," {createNavigation} ",[114,3887,2350],{"class":127},[114,3889,3794],{"class":1321},[114,3891,171],{"class":137},[114,3893,3894,3896,3899,3901,3904],{"class":116,"line":124},[114,3895,3676],{"class":127},[114,3897,3898],{"class":137}," {routing} ",[114,3900,2350],{"class":127},[114,3902,3903],{"class":1321}," '.\u002Frouting'",[114,3905,171],{"class":137},[114,3907,3908],{"class":116,"line":154},[114,3909,197],{"emptyLinePlaceholder":196},[114,3911,3912,3914,3916,3919,3922,3924,3927,3929,3932,3934,3937,3939,3942,3945],{"class":116,"line":174},[114,3913,3741],{"class":127},[114,3915,3807],{"class":127},[114,3917,3918],{"class":137}," {",[114,3920,3921],{"class":144},"Link",[114,3923,1085],{"class":137},[114,3925,3926],{"class":144},"redirect",[114,3928,1085],{"class":137},[114,3930,3931],{"class":144},"usePathname",[114,3933,1085],{"class":137},[114,3935,3936],{"class":144},"useRouter",[114,3938,1085],{"class":137},[114,3940,3941],{"class":144},"getPathname",[114,3943,3944],{"class":137},"} ",[114,3946,3947],{"class":127},"=\n",[114,3949,3950,3953],{"class":116,"line":193},[114,3951,3952],{"class":212},"  createNavigation",[114,3954,3955],{"class":137},"(routing);\n",[11,3957,3959],{"id":3958},"step-4-middleware-that-adapts-like-a-good-listener","Step 4: Middleware That Adapts Like a Good Listener",[16,3961,3962],{},"Middleware lets your app greet users in their language from the very first request.",[104,3964,3966],{"className":1954,"code":3965,"language":1956,"meta":109,"style":109},"import createMiddleware from \"next-intl\u002Fmiddleware\";\nimport { routing } from \".\u002Fi18n\u002Frouting\";\n\nexport default createMiddleware(routing);\nexport const config = {\n matcher: [\"\u002F((?!_next|api|ingest|.*\\\\..*).*)\"],\n};\n",[111,3967,3968,3982,3996,4000,4011,4024,4040],{"__ignoreMap":109},[114,3969,3970,3972,3975,3977,3980],{"class":116,"line":117},[114,3971,3676],{"class":127},[114,3973,3974],{"class":137}," createMiddleware ",[114,3976,2350],{"class":127},[114,3978,3979],{"class":1321}," \"next-intl\u002Fmiddleware\"",[114,3981,171],{"class":137},[114,3983,3984,3986,3989,3991,3994],{"class":116,"line":124},[114,3985,3676],{"class":127},[114,3987,3988],{"class":137}," { routing } ",[114,3990,2350],{"class":127},[114,3992,3993],{"class":1321}," \".\u002Fi18n\u002Frouting\"",[114,3995,171],{"class":137},[114,3997,3998],{"class":116,"line":154},[114,3999,197],{"emptyLinePlaceholder":196},[114,4001,4002,4004,4006,4009],{"class":116,"line":174},[114,4003,3741],{"class":127},[114,4005,3744],{"class":127},[114,4007,4008],{"class":212}," createMiddleware",[114,4010,3955],{"class":137},[114,4012,4013,4015,4017,4020,4022],{"class":116,"line":193},[114,4014,3741],{"class":127},[114,4016,3807],{"class":127},[114,4018,4019],{"class":144}," config",[114,4021,2229],{"class":127},[114,4023,216],{"class":137},[114,4025,4026,4029,4032,4035,4038],{"class":116,"line":200},[114,4027,4028],{"class":137}," matcher: [",[114,4030,4031],{"class":1321},"\"\u002F((?!_next|api|ingest|.*",[114,4033,4034],{"class":144},"\\\\",[114,4036,4037],{"class":1321},"..*).*)\"",[114,4039,3844],{"class":137},[114,4041,4042],{"class":116,"line":206},[114,4043,245],{"class":137},[11,4045,4047],{"id":4046},"step-5-responding-based-on-locale-context","Step 5: Responding Based on Locale Context",[16,4049,4050],{},"Server components need to know what language they're speaking. That's where request.ts comes in.",[16,4052,4053],{},[536,4054,4055],{},[584,4056,4057],{},"src\u002Fi18n\u002Frequest.ts",[104,4059,4061],{"className":1954,"code":4060,"language":1956,"meta":109,"style":109},"import type { Locale } from \"@\u002Fmessages\u002F_schema\";\nimport { getRequestConfig } from \"next-intl\u002Fserver\";\nimport { routing } from \".\u002Frouting\";\n\nexport default getRequestConfig(async ({ requestLocale }) => {\n \u002F\u002F This typically corresponds to the `[locale]` segment\n let locale = await requestLocale;\n\n if (!locale || !routing.locales.includes(locale as Locale)) {\n  locale = routing.defaultLocale;\n }\n\nreturn {\n  locale,\n  messages: (await import(`..\u002Fmessages\u002F${locale}.json`)).default,\n };\n});\n",[111,4062,4063,4080,4094,4107,4111,4139,4144,4159,4163,4199,4209,4214,4218,4224,4229,4252,4257],{"__ignoreMap":109},[114,4064,4065,4067,4070,4073,4075,4078],{"class":116,"line":117},[114,4066,3676],{"class":127},[114,4068,4069],{"class":127}," type",[114,4071,4072],{"class":137}," { Locale } ",[114,4074,2350],{"class":127},[114,4076,4077],{"class":1321}," \"@\u002Fmessages\u002F_schema\"",[114,4079,171],{"class":137},[114,4081,4082,4084,4087,4089,4092],{"class":116,"line":124},[114,4083,3676],{"class":127},[114,4085,4086],{"class":137}," { getRequestConfig } ",[114,4088,2350],{"class":127},[114,4090,4091],{"class":1321}," \"next-intl\u002Fserver\"",[114,4093,171],{"class":137},[114,4095,4096,4098,4100,4102,4105],{"class":116,"line":154},[114,4097,3676],{"class":127},[114,4099,3988],{"class":137},[114,4101,2350],{"class":127},[114,4103,4104],{"class":1321}," \".\u002Frouting\"",[114,4106,171],{"class":137},[114,4108,4109],{"class":116,"line":174},[114,4110,197],{"emptyLinePlaceholder":196},[114,4112,4113,4115,4117,4120,4122,4125,4128,4131,4134,4137],{"class":116,"line":193},[114,4114,3741],{"class":127},[114,4116,3744],{"class":127},[114,4118,4119],{"class":212}," getRequestConfig",[114,4121,299],{"class":137},[114,4123,4124],{"class":127},"async",[114,4126,4127],{"class":137}," ({ ",[114,4129,4130],{"class":2361},"requestLocale",[114,4132,4133],{"class":137}," }) ",[114,4135,4136],{"class":127},"=>",[114,4138,216],{"class":137},[114,4140,4141],{"class":116,"line":200},[114,4142,4143],{"class":120}," \u002F\u002F This typically corresponds to the `[locale]` segment\n",[114,4145,4146,4149,4152,4154,4156],{"class":116,"line":206},[114,4147,4148],{"class":127}," let",[114,4150,4151],{"class":137}," locale ",[114,4153,141],{"class":127},[114,4155,2232],{"class":127},[114,4157,4158],{"class":137}," requestLocale;\n",[114,4160,4161],{"class":116,"line":219},[114,4162,197],{"emptyLinePlaceholder":196},[114,4164,4165,4168,4170,4172,4175,4178,4181,4184,4187,4190,4193,4196],{"class":116,"line":228},[114,4166,4167],{"class":127}," if",[114,4169,418],{"class":137},[114,4171,3365],{"class":127},[114,4173,4174],{"class":137},"locale ",[114,4176,4177],{"class":127},"||",[114,4179,4180],{"class":127}," !",[114,4182,4183],{"class":137},"routing.locales.",[114,4185,4186],{"class":212},"includes",[114,4188,4189],{"class":137},"(locale ",[114,4191,4192],{"class":127},"as",[114,4194,4195],{"class":212}," Locale",[114,4197,4198],{"class":137},")) {\n",[114,4200,4201,4204,4206],{"class":116,"line":236},[114,4202,4203],{"class":137},"  locale ",[114,4205,141],{"class":127},[114,4207,4208],{"class":137}," routing.defaultLocale;\n",[114,4210,4211],{"class":116,"line":242},[114,4212,4213],{"class":137}," }\n",[114,4215,4216],{"class":116,"line":248},[114,4217,197],{"emptyLinePlaceholder":196},[114,4219,4220,4222],{"class":116,"line":253},[114,4221,1963],{"class":127},[114,4223,216],{"class":137},[114,4225,4226],{"class":116,"line":401},[114,4227,4228],{"class":137},"  locale,\n",[114,4230,4231,4234,4236,4239,4241,4244,4246,4249],{"class":116,"line":407},[114,4232,4233],{"class":137},"  messages: (",[114,4235,2213],{"class":127},[114,4237,4238],{"class":127}," import",[114,4240,299],{"class":137},[114,4242,4243],{"class":1321},"`..\u002Fmessages\u002F${",[114,4245,3523],{"class":137},[114,4247,4248],{"class":1321},"}.json`",[114,4250,4251],{"class":137},")).default,\n",[114,4253,4254],{"class":116,"line":412},[114,4255,4256],{"class":137}," };\n",[114,4258,4259],{"class":116,"line":437},[114,4260,3866],{"class":137},[11,4262,4264],{"id":4263},"step-6-set-up-your-locale-layout","Step 6: Set Up Your Locale Layout",[16,4266,4267],{},"Now we make sure every page honors the selected language.",[104,4269,4271],{"className":1193,"code":4270,"language":1195,"meta":109,"style":109},"import {NextIntlClientProvider, hasLocale} from 'next-intl';\nimport {notFound} from 'next\u002Fnavigation';\nimport {routing} from '@\u002Fi18n\u002Frouting';\n\nexport default async function LocaleLayout({\n  children,\n  params\n}: {\n  children: React.ReactNode;\n  params: Promise\u003C{locale: string}>;\n}) {\n  \u002F\u002F Ensure that the incoming `locale` is valid\n  const {locale} = await params;\n  if (!hasLocale(routing.locales, locale)) {\n    notFound();\n  }\n\n  return (\n    \u003Chtml lang={locale}>\n      \u003Cbody>\n        \u003CNextIntlClientProvider>{children}\u003C\u002FNextIntlClientProvider>\n      \u003C\u002Fbody>\n    \u003C\u002Fhtml>\n  );\n}\n",[111,4272,4273,4278,4283,4288,4292,4297,4302,4307,4312,4317,4322,4327,4332,4337,4342,4347,4351,4355,4361,4367,4373,4379,4385,4391,4397],{"__ignoreMap":109},[114,4274,4275],{"class":116,"line":117},[114,4276,4277],{},"import {NextIntlClientProvider, hasLocale} from 'next-intl';\n",[114,4279,4280],{"class":116,"line":124},[114,4281,4282],{},"import {notFound} from 'next\u002Fnavigation';\n",[114,4284,4285],{"class":116,"line":154},[114,4286,4287],{},"import {routing} from '@\u002Fi18n\u002Frouting';\n",[114,4289,4290],{"class":116,"line":174},[114,4291,197],{"emptyLinePlaceholder":196},[114,4293,4294],{"class":116,"line":193},[114,4295,4296],{},"export default async function LocaleLayout({\n",[114,4298,4299],{"class":116,"line":200},[114,4300,4301],{},"  children,\n",[114,4303,4304],{"class":116,"line":206},[114,4305,4306],{},"  params\n",[114,4308,4309],{"class":116,"line":219},[114,4310,4311],{},"}: {\n",[114,4313,4314],{"class":116,"line":228},[114,4315,4316],{},"  children: React.ReactNode;\n",[114,4318,4319],{"class":116,"line":236},[114,4320,4321],{},"  params: Promise\u003C{locale: string}>;\n",[114,4323,4324],{"class":116,"line":242},[114,4325,4326],{},"}) {\n",[114,4328,4329],{"class":116,"line":248},[114,4330,4331],{},"  \u002F\u002F Ensure that the incoming `locale` is valid\n",[114,4333,4334],{"class":116,"line":253},[114,4335,4336],{},"  const {locale} = await params;\n",[114,4338,4339],{"class":116,"line":401},[114,4340,4341],{},"  if (!hasLocale(routing.locales, locale)) {\n",[114,4343,4344],{"class":116,"line":407},[114,4345,4346],{},"    notFound();\n",[114,4348,4349],{"class":116,"line":412},[114,4350,404],{},[114,4352,4353],{"class":116,"line":437},[114,4354,197],{"emptyLinePlaceholder":196},[114,4356,4358],{"class":116,"line":4357},18,[114,4359,4360],{},"  return (\n",[114,4362,4364],{"class":116,"line":4363},19,[114,4365,4366],{},"    \u003Chtml lang={locale}>\n",[114,4368,4370],{"class":116,"line":4369},20,[114,4371,4372],{},"      \u003Cbody>\n",[114,4374,4376],{"class":116,"line":4375},21,[114,4377,4378],{},"        \u003CNextIntlClientProvider>{children}\u003C\u002FNextIntlClientProvider>\n",[114,4380,4382],{"class":116,"line":4381},22,[114,4383,4384],{},"      \u003C\u002Fbody>\n",[114,4386,4388],{"class":116,"line":4387},23,[114,4389,4390],{},"    \u003C\u002Fhtml>\n",[114,4392,4394],{"class":116,"line":4393},24,[114,4395,4396],{},"  );\n",[114,4398,4400],{"class":116,"line":4399},25,[114,4401,440],{},[11,4403,4405],{"id":4404},"step-7-let-your-users-feel-heard","Step 7: Let Your Users Feel Heard",[16,4407,4408],{},"In your components, bring the translations to life:",[16,4410,4411],{},[584,4412,4413],{},"Client Component",[104,4415,4417],{"className":1193,"code":4416,"language":1195,"meta":109,"style":109},"import {useTranslations} from 'next-intl';\nimport {Link} from '@\u002Fi18n\u002Fnavigation';\n\nexport default function HomePage() {\n  const t = useTranslations('HomePage');\n  return (\n    \u003Cdiv>\n      \u003Ch1>{t('title')}\u003C\u002Fh1>\n      \u003CLink href=\"\u002Fabout\">{t('about')}\u003C\u002FLink>\n    \u003C\u002Fdiv>\n  );\n}\n",[111,4418,4419,4424,4429,4433,4438,4443,4447,4452,4457,4462,4467,4471],{"__ignoreMap":109},[114,4420,4421],{"class":116,"line":117},[114,4422,4423],{},"import {useTranslations} from 'next-intl';\n",[114,4425,4426],{"class":116,"line":124},[114,4427,4428],{},"import {Link} from '@\u002Fi18n\u002Fnavigation';\n",[114,4430,4431],{"class":116,"line":154},[114,4432,197],{"emptyLinePlaceholder":196},[114,4434,4435],{"class":116,"line":174},[114,4436,4437],{},"export default function HomePage() {\n",[114,4439,4440],{"class":116,"line":193},[114,4441,4442],{},"  const t = useTranslations('HomePage');\n",[114,4444,4445],{"class":116,"line":200},[114,4446,4360],{},[114,4448,4449],{"class":116,"line":206},[114,4450,4451],{},"    \u003Cdiv>\n",[114,4453,4454],{"class":116,"line":219},[114,4455,4456],{},"      \u003Ch1>{t('title')}\u003C\u002Fh1>\n",[114,4458,4459],{"class":116,"line":228},[114,4460,4461],{},"      \u003CLink href=\"\u002Fabout\">{t('about')}\u003C\u002FLink>\n",[114,4463,4464],{"class":116,"line":236},[114,4465,4466],{},"    \u003C\u002Fdiv>\n",[114,4468,4469],{"class":116,"line":242},[114,4470,4396],{},[114,4472,4473],{"class":116,"line":248},[114,4474,440],{},[16,4476,4477],{},[584,4478,4479],{},"Server Component",[104,4481,4483],{"className":1193,"code":4482,"language":1195,"meta":109,"style":109},"import {getTranslations} from 'next-intl\u002Fserver';\n\nexport default async function HomePage() {\n  const t = await getTranslations('HomePage');\n  return \u003Ch1>{t('title')}\u003C\u002Fh1>;\n}\n",[111,4484,4485,4490,4494,4499,4504,4509],{"__ignoreMap":109},[114,4486,4487],{"class":116,"line":117},[114,4488,4489],{},"import {getTranslations} from 'next-intl\u002Fserver';\n",[114,4491,4492],{"class":116,"line":124},[114,4493,197],{"emptyLinePlaceholder":196},[114,4495,4496],{"class":116,"line":154},[114,4497,4498],{},"export default async function HomePage() {\n",[114,4500,4501],{"class":116,"line":174},[114,4502,4503],{},"  const t = await getTranslations('HomePage');\n",[114,4505,4506],{"class":116,"line":193},[114,4507,4508],{},"  return \u003Ch1>{t('title')}\u003C\u002Fh1>;\n",[114,4510,4511],{"class":116,"line":200},[114,4512,440],{},[36,4514,4516],{"id":4515},"not-every-conversation-starts-with-a-word","Not Every Conversation Starts With a Word",[16,4518,4519,4520,4523],{},"Sometimes, the most thoughtful form of listening is quiet observation. Instead of putting the language in the URL, we let the app detect the user's preferences and adapt silently. This is where ",[584,4521,4522],{},"listening without i18n routing"," shines — cleaner URLs, language chosen by the user, and a more fluid experience for apps where explicit routing isn't needed.",[16,4525,4526,4527],{},"If that approach sounds more like the kind of experience you want to create, next-intl supports it beautifully. You can explore that approach here:\n👉 ",[83,4528,4531],{"href":4529,"rel":4530},"https:\u002F\u002Fnext-intl.dev\u002Fdocs\u002Fgetting-started\u002Fapp-router\u002Fwithout-i18n-routing",[87],"Listening Without i18n Routing →",[36,4533,4535],{"id":4534},"speak-their-language-feel-their-world","Speak Their Language, Feel Their World",[16,4537,4538],{},"In the end, being heard is about more than just the words we say — it's about how we make people feel. Whether it's through visible cues like URLs or quiet detection of preferences, the heart of internationalization is empathy. It's about designing with the listener in mind.",[16,4540,4541],{},"With next-intl and Next.js, you're not just building apps — you're creating spaces that speak to people, in their language, on their terms.",[16,4543,4544],{},"And that's the kind of conversation worth having.",[968,4546,4547],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":109,"searchDepth":124,"depth":124,"links":4549},[4550,4551,4555,4556,4565,4566],{"id":3443,"depth":154,"text":3444},{"id":3494,"depth":124,"text":3495,"children":4552},[4553,4554],{"id":3507,"depth":154,"text":3508},{"id":3533,"depth":154,"text":3534},{"id":3554,"depth":124,"text":3555},{"id":3590,"depth":124,"text":3591,"children":4557},[4558,4559,4560,4561,4562,4563,4564],{"id":3606,"depth":154,"text":3607},{"id":3655,"depth":154,"text":3656},{"id":3752,"depth":154,"text":3753},{"id":3958,"depth":154,"text":3959},{"id":4046,"depth":154,"text":4047},{"id":4263,"depth":154,"text":4264},{"id":4404,"depth":154,"text":4405},{"id":4515,"depth":124,"text":4516},{"id":4534,"depth":124,"text":4535},"https:\u002F\u002Fiamlope.medium.com\u002Fbeing-heard-isnt-just-about-speaking-it-s-about-understanding-b700929c5743","2025-05-26","A guide to internationalization (i18n) in Next.js using next-intl to build multilingual, user-aware web experiences.",{},"\u002Fblog\u002Fbeing-heard",{"title":3438,"description":4569},"blog\u002Fbeing-heard","voR_PtRCUderky5gqAe_GcWaF7C_0v_GdrtYLfP717E",{"id":4576,"title":4577,"body":4578,"canonical":5927,"date":5928,"description":5929,"draft":995,"duration":992,"extension":996,"lang":992,"meta":5930,"navigation":196,"path":5931,"place":992,"placeLink":992,"readingTime":992,"seo":5932,"stem":5933,"__hash__":5934},"blog\u002Fblog\u002Fstop-yapping-lock-in.md","Stop yapping, Lock in",{"type":8,"value":4579,"toc":5915},[4580,4585,4590,4595,4598,4601,4604,4607,4610,4613,4616,4619,4622,4625,4629,4632,4635,4638,4641,4644,4647,4650,4653,4656,4659,4662,4665,4673,4676,4679,4682,4685,4689,4692,4695,4698,4701,4707,4710,4716,4736,4742,4745,4751,4758,4763,4766,4770,4773,4777,4783,4801,4805,4815,4840,4844,4847,4852,5068,5072,5075,5082,5422,5436,5454,5804,5808,5814,5819,5824,5829,5834,5838,5841,5844,5851,5858,5864,5869,5906,5912],[4581,4582,4584],"h4",{"id":4583},"keep-your-next-action-safe-weve-all-overshared-but-now-you-know-better-dont-cast-your-actions-be-legendary-and-anon","Keep your Next action safe — we've all overshared, but now you know better. Don't cast your actions… be legendary and anon.",[16,4586,4587],{},[19,4588],{"alt":109,"src":4589,"fetchPriority":23,"loading":24},"https:\u002F\u002Fcdn-images-1.medium.com\u002Fmax\u002F736\u002F1*AYssJZ41GpzFIedW-rC-ig.jpeg",[16,4591,4592],{},[536,4593,4594],{},"stay locked in!",[16,4596,4597],{},"It's late at night. What could go wrong?",[16,4599,4600],{},"A chill evening, breezy winds outside. I'd just devoured a warm bowl of apples, chicken, and plantain, chased it with cold yoghurt. Life was vibing. My screen glowed with a beautiful anime scene, and everything just felt right. Soft belly, clear mind, nothing serious.",[16,4602,4603],{},"Then my phone lit up.\nA call from an old friend. Random, unexpected.",[16,4605,4606],{},"I picked up.\nWe talked. Laughed a little. Then out of nowhere, they said something. Not loud, not rude… just odd. Misplaced. One of those lines that slips through a crack in the moment and hits you sideways. I couldn't even react. I just sat there, phone to my ear, feeling this quiet shift from comfort to confusion.",[16,4608,4609],{},"I ended the call. Not angrily. Just… done.\nYou know that feeling, right? Where something harmless turns heavy. Where you suddenly realize you're more vulnerable than you thought.",[16,4611,4612],{},"It stuck with me. And it reminded me of another time.",[16,4614,4615],{},"Back in junior high, I said something wild about a teacher's daughter. I don't even know why. It just came out. The word got around, and I got dragged. I remember sitting in agriculture class that day, trying to focus on crop rotation while my brain was busy rotating my entire life. I wanted to disappear.",[16,4617,4618],{},"We still talk now, me and the person. We laugh about it. But I never forgot how one careless sentence changed everything for a while. It's wild how fast things can shift when you're unguarded. When you speak before filtering. When you share without thinking.",[16,4620,4621],{},"That's kind of how a lot of apps behave. You build something beautiful. A clean UI, smooth animations, everything chill. Then out of nowhere, a request comes in. Not a user you expected, not a flow you tested. And your server… just responds. Says too much. Does too much. No filters. No checks.",[16,4623,4624],{},"And just like that, you're leaking data. Performing actions you shouldn't. Breaking trust. Not because you meant to — but because you didn't stop to lock in.",[36,4626,4628],{"id":4627},"stop-yapping-your-servers-saying-too-much","Stop Yapping: Your Server's Saying Too Much",[16,4630,4631],{},"See, it's easy to laugh at a wild comment in a conversation or a slip-up back in junior high — until your app does the exact same thing.",[16,4633,4634],{},"You start with good intentions. You've been building for days, maybe weeks. The UI looks tight. Buttons glow just right, hover states are smooth, and the animations flow like that cold yoghurt I mentioned earlier. You click through the flows — login, dashboard, a few form submissions — everything feels buttery. You nod to yourself. \"Solid,\" you think. \"Safe.\"",[16,4636,4637],{},"But safety, it turns out, is often a feeling, not a fact.",[16,4639,4640],{},"You feel safe because you used the app the way it was supposed to be used. But apps live in the wild, and in the wild, people don't follow scripts. They mistype. They reload mid-request. They submit empty forms. Some dig deeper — inspect elements, open DevTools, and start sending all sorts of unexpected things to your server. And then there are the clever ones. The folks who skip your UI entirely and go straight for the real prize: your server actions.",[16,4642,4643],{},"They send direct fetch requests. They guess routes. They trigger functions that were never meant for them. No UI. No animation. No questions asked.",[16,4645,4646],{},"And if your server isn't ready — if it doesn't pause for a moment and ask, \"Wait, should I be doing this?\" — then it just responds. No checks, no guards, no sanity.",[16,4648,4649],{},"That's how a clean app becomes a loud liability.",[16,4651,4652],{},"The polish on your frontend doesn't matter if the logic underneath is just blurting things out. Your backend starts oversharing. Suddenly, a random user is deleting data they shouldn't even see. Or creating records. Or accessing private information. Not because they broke in, but because you left the door wide open.",[16,4654,4655],{},"It's like leaving your diary on a park bench and wondering why someone read it.",[16,4657,4658],{},"And you pay for it — literally. You start seeing inflated bills from background jobs gone rogue. Logs overflowing with nonsense. Your database filled with junk entries. Worst of all, you lose credibility. People stop trusting the product. Not because it looks bad — but because it couldn't stay quiet when it mattered.",[16,4660,4661],{},"That's the danger of a backend that doesn't lock in. One that just keeps yapping.",[16,4663,4664],{},"Look, it's okay to talk. It's okay to open up. But solid words — intentional words — carry more weight than a flurry of unfiltered noise. Even chefs say a cook that talks too much probably doesn't know how to cook. The same goes for apps. The ones that speak too freely often aren't built with care.",[16,4666,4667,4672],{},[83,4668,4671],{"href":4669,"rel":4670},"https:\u002F\u002Fnext-safe-action.dev\u002Fdocs\u002Fgetting-started",[87],"next-safe-action"," changes that.",[16,4674,4675],{},"It gives your app structure. It gives your server guardrails. Instead of instantly responding to any request, it gives you a clean pattern to validate intent, check for permissions, and control who gets to do what. It's like having a bouncer at the door of every server action, asking, \"Who sent you?\" before anything even happens.",[16,4677,4678],{},"Even better, it builds in a healthy pause. A moment of state — pending, success, error. That delay isn't just for UX polish. It's the space where you think, where the app confirms, \"Yes, I'm doing the right thing.\"",[16,4680,4681],{},"No blurting. No oversharing. Just clarity.",[16,4683,4684],{},"That's what locking in looks like.",[36,4686,4688],{"id":4687},"the-cost-of-silence-why-locking-in-is-the-smart-move","The Cost of Silence: Why Locking In Is the Smart Move",[16,4690,4691],{},"So, we've established that oversharing in code can break your app. But let's dig deeper into what happens when your server finally locks in.",[16,4693,4694],{},"Imagine you're hosting a party. You've got the guest list covered — friends, coworkers, everyone you trust. The atmosphere is relaxed, the conversation is flowing, and the vibe is great. But then, someone unexpected shows up. No invite. They don't know the rules. They walk right in, help themselves to the snacks, and sit down on the couch without even saying hello.",[16,4696,4697],{},"At first, it's not a big deal. They're quiet. They blend in. But over time, things start to shift. People begin noticing. Conversations stop. You start hearing whispers. Who is this person? Why are they here? What are they doing?",[16,4699,4700],{},"That's what unfiltered access does to your server. It opens the door for random, unvalidated requests to come in. Initially, it might seem harmless. But over time, those unknown requests stack up. They start to shift your app's behavior, compromise its security, and eat away at its credibility.",[16,4702,4703,4704,4706],{},"And this is where ",[584,4705,4671],{}," comes in.",[16,4708,4709],{},"Next-safe-action is like the velvet rope at the door, carefully managing who gets in and when. It ensures only trusted actions are allowed to pass through. This is about creating a safe environment — not to be rude, but to establish the trust your app needs to thrive.",[16,4711,4712,4713,4715],{},"When you lock in your server actions with ",[584,4714,4671],{},", it gives your app a sense of order and control. Here's what it does:",[2639,4717,4718,4724,4730],{},[720,4719,4720,4723],{},[584,4721,4722],{},"Type Safety",": Every input is validated. Your server knows exactly what to expect. No more garbage data sneaking through.",[720,4725,4726,4729],{},[584,4727,4728],{},"Zod-Powered Validation",": It checks the validity of data before letting it through, ensuring users can't send malicious or malformed data.",[720,4731,4732,4735],{},[584,4733,4734],{},"Server-Side Control",": Actions are only triggered from the client where you explicitly allow them, giving you full control over who can do what.",[16,4737,4738,4739,4741],{},"Think of it like this: ",[584,4740,4671],{}," doesn't just put a filter on your server; it teaches it how to think. It's like your server now knows when to pause and ask, \"Wait, am I doing this correctly?\" before responding. It's about creating that feedback loop that helps both you and your app stay on track.",[16,4743,4744],{},"When your server locks in, it doesn't just protect data — it safeguards the entire user experience. With that extra layer of thought, it doesn't get overwhelmed by random requests or sloppy inputs. The system doesn't buckle. It responds predictably, and it's safe.",[16,4746,4747,4748,4750],{},"And here's the bonus: locking in with ",[584,4749,4671],{}," means less processing power wasted on rogue requests. No unnecessary retries. No timeouts. You'll start seeing fewer bills because your server isn't spinning its wheels on unwanted or invalid actions.",[16,4752,4753,4754,4757],{},"It's like a chef in a kitchen. They don't throw everything in the pot hoping it'll taste good. They know exactly what ingredients are needed, when they're needed, and how much to use. ",[584,4755,4756],{},"Next-safe-action"," does the same for your server. It makes sure the actions are clear, well-defined, and executed with care.",[16,4759,4760,4762],{},[584,4761,4756],{}," is more than just a safeguard against attackers. It's about ensuring your app doesn't make mistakes out of eagerness. It doesn't overshare. It doesn't act recklessly. It only does exactly what it's meant to do — and only when it's supposed to.",[16,4764,4765],{},"This level of control builds trust. When users know they're interacting with an app that's not going to overshare, they'll use it more. They'll trust it more. And that's the kind of app users keep coming back to.",[36,4767,4769],{"id":4768},"enough-talk-lets-lock-in","Enough Talk, Let's Lock In",[16,4771,4772],{},"Alright, we've set the scene. We've talked about the risks, the consequences, and the need for locking in. But how do you actually get started? Simple.",[11,4774,4776],{"id":4775},"step-1-install-the-dependencies","Step 1: Install the Dependencies",[16,4778,4779,4780],{},"You're just one command away from protecting your server with ",[584,4781,4782],{},"next-safe-action:",[104,4784,4786],{"className":1309,"code":4785,"language":1311,"meta":109,"style":109},"pnpm add next-safe-action zod\n",[111,4787,4788],{"__ignoreMap":109},[114,4789,4790,4793,4795,4798],{"class":116,"line":117},[114,4791,4792],{"class":212},"pnpm",[114,4794,1328],{"class":1321},[114,4796,4797],{"class":1321}," next-safe-action",[114,4799,4800],{"class":1321}," zod\n",[11,4802,4804],{"id":4803},"why-zod","Why Zod?",[16,4806,4807,4808,4811,4812,4814],{},"Why ",[584,4809,4810],{},"Zod","? Because it's a powerful, type-safe validation library that works great with TypeScript and integrates seamlessly with ",[584,4813,4671],{},". It ensures your inputs are validated correctly before any action is executed.",[16,4816,4817,4818,4820,4821,4828,4829,4832,4833,4836,4837,4839],{},"But hey, I get it — everyone has their own preferences. If you're more comfortable with another validation library, feel free to swap out ",[584,4819,4810],{},". You could try ",[83,4822,4825],{"href":4823,"rel":4824},"https:\u002F\u002Fvalibot.dev\u002F",[87],[584,4826,4827],{},"Valibot"," (a fresh, up-and-coming option), ",[584,4830,4831],{},"Yup",", or ",[584,4834,4835],{},"TypeBox",". The choice is yours, but ",[584,4838,4810],{}," is a personal favorite for type safety.",[11,4841,4843],{"id":4842},"step-2-define-safe-actions","Step 2: Define Safe Actions",[16,4845,4846],{},"Once you've installed the necessary packages, you can start defining your server actions with safety in mind.",[16,4848,4849,4850,1987],{},"Here's a basic example of how you might set up a server action with ",[584,4851,4671],{},[104,4853,4855],{"className":1954,"code":4854,"language":1956,"meta":109,"style":109},"import {\n  DEFAULT_SERVER_ERROR_MESSAGE,\n  createSafeActionClient,\n} from \"next-safe-action\";\n\n\u002F\u002F Base client with error handling and middleware\nexport const actionClient = createSafeActionClient({\n  handleServerError(e: Error | { errors?: Record\u003Cstring, string> }) {\n    console.error(\"Action error:\", e.message);\n\n    if (e instanceof Error) {\n      return e.message;\n    }\n\n    return DEFAULT_SERVER_ERROR_MESSAGE;\n  },\n}).use(async ({ next }) => {\n  const result = await next();\n  return result;\n});\n",[111,4856,4857,4863,4868,4873,4884,4888,4893,4909,4952,4968,4972,4987,4995,5000,5004,5013,5018,5040,5057,5064],{"__ignoreMap":109},[114,4858,4859,4861],{"class":116,"line":117},[114,4860,3676],{"class":127},[114,4862,216],{"class":137},[114,4864,4865],{"class":116,"line":124},[114,4866,4867],{"class":137},"  DEFAULT_SERVER_ERROR_MESSAGE,\n",[114,4869,4870],{"class":116,"line":154},[114,4871,4872],{"class":137},"  createSafeActionClient,\n",[114,4874,4875,4877,4879,4882],{"class":116,"line":174},[114,4876,3944],{"class":137},[114,4878,2350],{"class":127},[114,4880,4881],{"class":1321}," \"next-safe-action\"",[114,4883,171],{"class":137},[114,4885,4886],{"class":116,"line":193},[114,4887,197],{"emptyLinePlaceholder":196},[114,4889,4890],{"class":116,"line":200},[114,4891,4892],{"class":120},"\u002F\u002F Base client with error handling and middleware\n",[114,4894,4895,4897,4899,4902,4904,4907],{"class":116,"line":206},[114,4896,3741],{"class":127},[114,4898,3807],{"class":127},[114,4900,4901],{"class":144}," actionClient",[114,4903,2229],{"class":127},[114,4905,4906],{"class":212}," createSafeActionClient",[114,4908,3818],{"class":137},[114,4910,4911,4914,4916,4919,4921,4924,4927,4930,4933,4936,4939,4942,4945,4947,4949],{"class":116,"line":219},[114,4912,4913],{"class":212},"  handleServerError",[114,4915,299],{"class":137},[114,4917,4918],{"class":2361},"e",[114,4920,1987],{"class":127},[114,4922,4923],{"class":212}," Error",[114,4925,4926],{"class":127}," |",[114,4928,4929],{"class":137}," { ",[114,4931,4932],{"class":2361},"errors",[114,4934,4935],{"class":127},"?:",[114,4937,4938],{"class":212}," Record",[114,4940,4941],{"class":137},"\u003C",[114,4943,4944],{"class":144},"string",[114,4946,1085],{"class":137},[114,4948,4944],{"class":144},[114,4950,4951],{"class":137},"> }) {\n",[114,4953,4954,4957,4960,4962,4965],{"class":116,"line":228},[114,4955,4956],{"class":137},"    console.",[114,4958,4959],{"class":212},"error",[114,4961,299],{"class":137},[114,4963,4964],{"class":1321},"\"Action error:\"",[114,4966,4967],{"class":137},", e.message);\n",[114,4969,4970],{"class":116,"line":236},[114,4971,197],{"emptyLinePlaceholder":196},[114,4973,4974,4977,4980,4983,4985],{"class":116,"line":242},[114,4975,4976],{"class":127},"    if",[114,4978,4979],{"class":137}," (e ",[114,4981,4982],{"class":127},"instanceof",[114,4984,4923],{"class":212},[114,4986,385],{"class":137},[114,4988,4989,4992],{"class":116,"line":248},[114,4990,4991],{"class":127},"      return",[114,4993,4994],{"class":137}," e.message;\n",[114,4996,4997],{"class":116,"line":253},[114,4998,4999],{"class":137},"    }\n",[114,5001,5002],{"class":116,"line":401},[114,5003,197],{"emptyLinePlaceholder":196},[114,5005,5006,5008,5011],{"class":116,"line":407},[114,5007,390],{"class":127},[114,5009,5010],{"class":144}," DEFAULT_SERVER_ERROR_MESSAGE",[114,5012,171],{"class":137},[114,5014,5015],{"class":116,"line":412},[114,5016,5017],{"class":137},"  },\n",[114,5019,5020,5023,5026,5028,5030,5032,5034,5036,5038],{"class":116,"line":437},[114,5021,5022],{"class":137},"}).",[114,5024,5025],{"class":212},"use",[114,5027,299],{"class":137},[114,5029,4124],{"class":127},[114,5031,4127],{"class":137},[114,5033,2412],{"class":2361},[114,5035,4133],{"class":137},[114,5037,4136],{"class":127},[114,5039,216],{"class":137},[114,5041,5042,5045,5048,5050,5052,5055],{"class":116,"line":4357},[114,5043,5044],{"class":127},"  const",[114,5046,5047],{"class":144}," result",[114,5049,2229],{"class":127},[114,5051,2232],{"class":127},[114,5053,5054],{"class":212}," next",[114,5056,3736],{"class":137},[114,5058,5059,5061],{"class":116,"line":4363},[114,5060,415],{"class":127},[114,5062,5063],{"class":137}," result;\n",[114,5065,5066],{"class":116,"line":4369},[114,5067,3866],{"class":137},[11,5069,5071],{"id":5070},"step-3-write-your-first-safe-action","Step 3: Write Your First Safe Action",[16,5073,5074],{},"Now let's put it to use.",[16,5076,5077,5078,5081],{},"Here's an example of a ",[584,5079,5080],{},"resend email verification"," action. We validate the input, send a request to the backend, and handle any errors cleanly — all while staying type-safe and guarded.",[104,5083,5085],{"className":1954,"code":5084,"language":1956,"meta":109,"style":109},"\"use server\";\n\nimport { z } from \"zod\";\nimport axios from \"axios\";\nimport { actionClient } from \"@\u002Flib\u002Fintegrations\u002Fsafe-action\";\nimport axiosInstance from \"@\u002Flib\u002Fintegrations\u002Faxios-wrapper\";\n\n\u002F\u002F Validation schema\nconst confirmEmailSchema = z.object({\n  email: z.string().email(),\n});\n\n\u002F\u002F action\nexport const resendVerification = actionClient\n  .schema(confirmEmailSchema)\n  .action(async ({ parsedInput }) => {\n    const { email } = parsedInput;\n\n    try {\n      await axiosInstance.post(\"\u002Fauth\u002Fsend_verification\", { email });\n\n      return {\n        success: true,\n        message: \"Please check your email for a verification link\",\n      };\n    } catch (error) {\n      if (axios.isAxiosError(error) && error.response?.data?.message) {\n        throw new Error(error.response.data.message);\n      }\n\n      throw new Error(\"Something went wrong. Please try again later.\");\n    }\n  });\n",[111,5086,5087,5094,5098,5112,5126,5140,5154,5158,5163,5180,5196,5200,5204,5209,5223,5234,5256,5273,5277,5284,5303,5307,5313,5323,5333,5338,5350,5370,5383,5389,5394,5411,5416],{"__ignoreMap":109},[114,5088,5089,5092],{"class":116,"line":117},[114,5090,5091],{"class":1321},"\"use server\"",[114,5093,171],{"class":137},[114,5095,5096],{"class":116,"line":124},[114,5097,197],{"emptyLinePlaceholder":196},[114,5099,5100,5102,5105,5107,5110],{"class":116,"line":154},[114,5101,3676],{"class":127},[114,5103,5104],{"class":137}," { z } ",[114,5106,2350],{"class":127},[114,5108,5109],{"class":1321}," \"zod\"",[114,5111,171],{"class":137},[114,5113,5114,5116,5119,5121,5124],{"class":116,"line":174},[114,5115,3676],{"class":127},[114,5117,5118],{"class":137}," axios ",[114,5120,2350],{"class":127},[114,5122,5123],{"class":1321}," \"axios\"",[114,5125,171],{"class":137},[114,5127,5128,5130,5133,5135,5138],{"class":116,"line":193},[114,5129,3676],{"class":127},[114,5131,5132],{"class":137}," { actionClient } ",[114,5134,2350],{"class":127},[114,5136,5137],{"class":1321}," \"@\u002Flib\u002Fintegrations\u002Fsafe-action\"",[114,5139,171],{"class":137},[114,5141,5142,5144,5147,5149,5152],{"class":116,"line":200},[114,5143,3676],{"class":127},[114,5145,5146],{"class":137}," axiosInstance ",[114,5148,2350],{"class":127},[114,5150,5151],{"class":1321}," \"@\u002Flib\u002Fintegrations\u002Faxios-wrapper\"",[114,5153,171],{"class":137},[114,5155,5156],{"class":116,"line":206},[114,5157,197],{"emptyLinePlaceholder":196},[114,5159,5160],{"class":116,"line":219},[114,5161,5162],{"class":120},"\u002F\u002F Validation schema\n",[114,5164,5165,5167,5170,5172,5175,5178],{"class":116,"line":228},[114,5166,128],{"class":127},[114,5168,5169],{"class":144}," confirmEmailSchema",[114,5171,2229],{"class":127},[114,5173,5174],{"class":137}," z.",[114,5176,5177],{"class":212},"object",[114,5179,3818],{"class":137},[114,5181,5182,5185,5187,5190,5193],{"class":116,"line":236},[114,5183,5184],{"class":137},"  email: z.",[114,5186,4944],{"class":212},[114,5188,5189],{"class":137},"().",[114,5191,5192],{"class":212},"email",[114,5194,5195],{"class":137},"(),\n",[114,5197,5198],{"class":116,"line":242},[114,5199,3866],{"class":137},[114,5201,5202],{"class":116,"line":248},[114,5203,197],{"emptyLinePlaceholder":196},[114,5205,5206],{"class":116,"line":253},[114,5207,5208],{"class":120},"\u002F\u002F action\n",[114,5210,5211,5213,5215,5218,5220],{"class":116,"line":401},[114,5212,3741],{"class":127},[114,5214,3807],{"class":127},[114,5216,5217],{"class":144}," resendVerification",[114,5219,2229],{"class":127},[114,5221,5222],{"class":137}," actionClient\n",[114,5224,5225,5228,5231],{"class":116,"line":407},[114,5226,5227],{"class":137},"  .",[114,5229,5230],{"class":212},"schema",[114,5232,5233],{"class":137},"(confirmEmailSchema)\n",[114,5235,5236,5238,5241,5243,5245,5247,5250,5252,5254],{"class":116,"line":412},[114,5237,5227],{"class":137},[114,5239,5240],{"class":212},"action",[114,5242,299],{"class":137},[114,5244,4124],{"class":127},[114,5246,4127],{"class":137},[114,5248,5249],{"class":2361},"parsedInput",[114,5251,4133],{"class":137},[114,5253,4136],{"class":127},[114,5255,216],{"class":137},[114,5257,5258,5261,5263,5265,5268,5270],{"class":116,"line":437},[114,5259,5260],{"class":127},"    const",[114,5262,4929],{"class":137},[114,5264,5192],{"class":144},[114,5266,5267],{"class":137}," } ",[114,5269,141],{"class":127},[114,5271,5272],{"class":137}," parsedInput;\n",[114,5274,5275],{"class":116,"line":4357},[114,5276,197],{"emptyLinePlaceholder":196},[114,5278,5279,5282],{"class":116,"line":4363},[114,5280,5281],{"class":127},"    try",[114,5283,216],{"class":137},[114,5285,5286,5289,5292,5295,5297,5300],{"class":116,"line":4369},[114,5287,5288],{"class":127},"      await",[114,5290,5291],{"class":137}," axiosInstance.",[114,5293,5294],{"class":212},"post",[114,5296,299],{"class":137},[114,5298,5299],{"class":1321},"\"\u002Fauth\u002Fsend_verification\"",[114,5301,5302],{"class":137},", { email });\n",[114,5304,5305],{"class":116,"line":4375},[114,5306,197],{"emptyLinePlaceholder":196},[114,5308,5309,5311],{"class":116,"line":4381},[114,5310,4991],{"class":127},[114,5312,216],{"class":137},[114,5314,5315,5318,5321],{"class":116,"line":4387},[114,5316,5317],{"class":137},"        success: ",[114,5319,5320],{"class":144},"true",[114,5322,225],{"class":137},[114,5324,5325,5328,5331],{"class":116,"line":4393},[114,5326,5327],{"class":137},"        message: ",[114,5329,5330],{"class":1321},"\"Please check your email for a verification link\"",[114,5332,225],{"class":137},[114,5334,5335],{"class":116,"line":4399},[114,5336,5337],{"class":137},"      };\n",[114,5339,5341,5344,5347],{"class":116,"line":5340},26,[114,5342,5343],{"class":137},"    } ",[114,5345,5346],{"class":127},"catch",[114,5348,5349],{"class":137}," (error) {\n",[114,5351,5353,5356,5359,5362,5365,5367],{"class":116,"line":5352},27,[114,5354,5355],{"class":127},"      if",[114,5357,5358],{"class":137}," (axios.",[114,5360,5361],{"class":212},"isAxiosError",[114,5363,5364],{"class":137},"(error) ",[114,5366,2679],{"class":127},[114,5368,5369],{"class":137}," error.response?.data?.message) {\n",[114,5371,5373,5376,5378,5380],{"class":116,"line":5372},28,[114,5374,5375],{"class":127},"        throw",[114,5377,2391],{"class":127},[114,5379,4923],{"class":212},[114,5381,5382],{"class":137},"(error.response.data.message);\n",[114,5384,5386],{"class":116,"line":5385},29,[114,5387,5388],{"class":137},"      }\n",[114,5390,5392],{"class":116,"line":5391},30,[114,5393,197],{"emptyLinePlaceholder":196},[114,5395,5397,5400,5402,5404,5406,5409],{"class":116,"line":5396},31,[114,5398,5399],{"class":127},"      throw",[114,5401,2391],{"class":127},[114,5403,4923],{"class":212},[114,5405,299],{"class":137},[114,5407,5408],{"class":1321},"\"Something went wrong. Please try again later.\"",[114,5410,305],{"class":137},[114,5412,5414],{"class":116,"line":5413},32,[114,5415,4999],{"class":137},[114,5417,5419],{"class":116,"line":5418},33,[114,5420,5421],{"class":137},"  });\n",[16,5423,5424,5425,5428,5429,5432,5433,449],{},"And that's it. Simple, clean, and secure — your server no longer just responds to anything. It ",[536,5426,5427],{},"thinks",", it ",[536,5430,5431],{},"filters",", and it ",[536,5434,5435],{},"locks in",[16,5437,5438,5439,5442,5443,5445,5446,5449,5450,5453],{},"To use this on the client, you simply define the user interface. While you ",[536,5440,5441],{},"could"," call the server action directly, ",[584,5444,4671],{}," also exposes a special hook for the client: ",[111,5447,5448],{},"useAction",". It's built not just for calling actions, but for listening, thinking, and responding. Think of it like a conversation—",[584,5451,5452],{},"pause, process, then speak",". That's the pattern.",[104,5455,5457],{"className":1193,"code":5456,"language":1195,"meta":109,"style":109},"\"use client\";\n\nimport { Button } from \"@\u002Fcomponents\u002Fui\u002Fbutton\";\nimport { DEFAULT_SERVER_ERROR_MESSAGE } from \"next-safe-action\";\nimport { useAction } from \"next-safe-action\u002Fhooks\";\nimport { useQueryState } from \"nuqs\";\nimport { toast } from \"sonner\";\nimport { resendVerification } from \"..\u002F_actions\";\nimport { useCountdown } from \"..\u002F_hooks\";\nimport { referralQueryParsers, referralTypeEnums } from \"..\u002F_schema\";\n\nconst RESEND_COUNTDOWN_SECONDS = 60;\n\nexport const ResendConfirmation = ({ email }: { email: string }) => {\n  const { countdown, startCountdown, isActive } = useCountdown(\n    RESEND_COUNTDOWN_SECONDS,\n  );\n  const [referral] = useQueryState(\"referral\", referralQueryParsers.referral);\n\n  const { execute, status } = useAction(resendVerification, {\n    onSuccess: (data) => {\n      toast.success(\n        data.data?.message || \"Verification email sent successfully\",\n      );\n    },\n    onError: (error) => {\n      toast.error(error.error.serverError);\n    },\n  });\n\n  const handleResend = async () => {\n    startCountdown();\n    execute({ email });\n  };\n\n  return (\n    \u003C>\n      {referral === referralTypeEnums.Enum[\"sign-up\"] ? (\n        \u003Cdiv className=\"mt-2 text-sm\">\n          Did not receive an email?{\" \"}\n          \u003CButton\n            className=\"p-0 text-blue-600 disabled:text-gray-400\"\n            onClick={handleResend}\n            disabled={isActive || status === \"executing\"}\n            variant=\"link\"\n          >\n            {isActive\n              ? `Wait ${countdown} seconds to resend`\n              : \"Click to resend\"}\n          \u003C\u002FButton>\n        \u003C\u002Fdiv>\n      ) : (\n        \u003CButton\n          onClick={handleResend}\n          className=\"w-full border border-blue-600 text-blue-600 bg-white\"\n          disabled={isActive || status === \"executing\"}\n        >\n          {isActive\n            ? `Wait ${countdown} seconds to resend`\n            : \"Request Verification\"}\n        \u003C\u002FButton>\n      )}\n    \u003C\u002F>\n  );\n};\n",[111,5458,5459,5464,5468,5473,5478,5483,5488,5493,5498,5503,5508,5512,5517,5521,5526,5531,5536,5540,5545,5549,5554,5559,5564,5569,5574,5579,5584,5589,5593,5597,5601,5606,5611,5616,5622,5627,5632,5638,5644,5650,5656,5662,5668,5674,5680,5686,5692,5698,5704,5710,5716,5722,5728,5734,5740,5746,5752,5758,5764,5770,5776,5782,5788,5794,5799],{"__ignoreMap":109},[114,5460,5461],{"class":116,"line":117},[114,5462,5463],{},"\"use client\";\n",[114,5465,5466],{"class":116,"line":124},[114,5467,197],{"emptyLinePlaceholder":196},[114,5469,5470],{"class":116,"line":154},[114,5471,5472],{},"import { Button } from \"@\u002Fcomponents\u002Fui\u002Fbutton\";\n",[114,5474,5475],{"class":116,"line":174},[114,5476,5477],{},"import { DEFAULT_SERVER_ERROR_MESSAGE } from \"next-safe-action\";\n",[114,5479,5480],{"class":116,"line":193},[114,5481,5482],{},"import { useAction } from \"next-safe-action\u002Fhooks\";\n",[114,5484,5485],{"class":116,"line":200},[114,5486,5487],{},"import { useQueryState } from \"nuqs\";\n",[114,5489,5490],{"class":116,"line":206},[114,5491,5492],{},"import { toast } from \"sonner\";\n",[114,5494,5495],{"class":116,"line":219},[114,5496,5497],{},"import { resendVerification } from \"..\u002F_actions\";\n",[114,5499,5500],{"class":116,"line":228},[114,5501,5502],{},"import { useCountdown } from \"..\u002F_hooks\";\n",[114,5504,5505],{"class":116,"line":236},[114,5506,5507],{},"import { referralQueryParsers, referralTypeEnums } from \"..\u002F_schema\";\n",[114,5509,5510],{"class":116,"line":242},[114,5511,197],{"emptyLinePlaceholder":196},[114,5513,5514],{"class":116,"line":248},[114,5515,5516],{},"const RESEND_COUNTDOWN_SECONDS = 60;\n",[114,5518,5519],{"class":116,"line":253},[114,5520,197],{"emptyLinePlaceholder":196},[114,5522,5523],{"class":116,"line":401},[114,5524,5525],{},"export const ResendConfirmation = ({ email }: { email: string }) => {\n",[114,5527,5528],{"class":116,"line":407},[114,5529,5530],{},"  const { countdown, startCountdown, isActive } = useCountdown(\n",[114,5532,5533],{"class":116,"line":412},[114,5534,5535],{},"    RESEND_COUNTDOWN_SECONDS,\n",[114,5537,5538],{"class":116,"line":437},[114,5539,4396],{},[114,5541,5542],{"class":116,"line":4357},[114,5543,5544],{},"  const [referral] = useQueryState(\"referral\", referralQueryParsers.referral);\n",[114,5546,5547],{"class":116,"line":4363},[114,5548,197],{"emptyLinePlaceholder":196},[114,5550,5551],{"class":116,"line":4369},[114,5552,5553],{},"  const { execute, status } = useAction(resendVerification, {\n",[114,5555,5556],{"class":116,"line":4375},[114,5557,5558],{},"    onSuccess: (data) => {\n",[114,5560,5561],{"class":116,"line":4381},[114,5562,5563],{},"      toast.success(\n",[114,5565,5566],{"class":116,"line":4387},[114,5567,5568],{},"        data.data?.message || \"Verification email sent successfully\",\n",[114,5570,5571],{"class":116,"line":4393},[114,5572,5573],{},"      );\n",[114,5575,5576],{"class":116,"line":4399},[114,5577,5578],{},"    },\n",[114,5580,5581],{"class":116,"line":5340},[114,5582,5583],{},"    onError: (error) => {\n",[114,5585,5586],{"class":116,"line":5352},[114,5587,5588],{},"      toast.error(error.error.serverError);\n",[114,5590,5591],{"class":116,"line":5372},[114,5592,5578],{},[114,5594,5595],{"class":116,"line":5385},[114,5596,5421],{},[114,5598,5599],{"class":116,"line":5391},[114,5600,197],{"emptyLinePlaceholder":196},[114,5602,5603],{"class":116,"line":5396},[114,5604,5605],{},"  const handleResend = async () => {\n",[114,5607,5608],{"class":116,"line":5413},[114,5609,5610],{},"    startCountdown();\n",[114,5612,5613],{"class":116,"line":5418},[114,5614,5615],{},"    execute({ email });\n",[114,5617,5619],{"class":116,"line":5618},34,[114,5620,5621],{},"  };\n",[114,5623,5625],{"class":116,"line":5624},35,[114,5626,197],{"emptyLinePlaceholder":196},[114,5628,5630],{"class":116,"line":5629},36,[114,5631,4360],{},[114,5633,5635],{"class":116,"line":5634},37,[114,5636,5637],{},"    \u003C>\n",[114,5639,5641],{"class":116,"line":5640},38,[114,5642,5643],{},"      {referral === referralTypeEnums.Enum[\"sign-up\"] ? (\n",[114,5645,5647],{"class":116,"line":5646},39,[114,5648,5649],{},"        \u003Cdiv className=\"mt-2 text-sm\">\n",[114,5651,5653],{"class":116,"line":5652},40,[114,5654,5655],{},"          Did not receive an email?{\" \"}\n",[114,5657,5659],{"class":116,"line":5658},41,[114,5660,5661],{},"          \u003CButton\n",[114,5663,5665],{"class":116,"line":5664},42,[114,5666,5667],{},"            className=\"p-0 text-blue-600 disabled:text-gray-400\"\n",[114,5669,5671],{"class":116,"line":5670},43,[114,5672,5673],{},"            onClick={handleResend}\n",[114,5675,5677],{"class":116,"line":5676},44,[114,5678,5679],{},"            disabled={isActive || status === \"executing\"}\n",[114,5681,5683],{"class":116,"line":5682},45,[114,5684,5685],{},"            variant=\"link\"\n",[114,5687,5689],{"class":116,"line":5688},46,[114,5690,5691],{},"          >\n",[114,5693,5695],{"class":116,"line":5694},47,[114,5696,5697],{},"            {isActive\n",[114,5699,5701],{"class":116,"line":5700},48,[114,5702,5703],{},"              ? `Wait ${countdown} seconds to resend`\n",[114,5705,5707],{"class":116,"line":5706},49,[114,5708,5709],{},"              : \"Click to resend\"}\n",[114,5711,5713],{"class":116,"line":5712},50,[114,5714,5715],{},"          \u003C\u002FButton>\n",[114,5717,5719],{"class":116,"line":5718},51,[114,5720,5721],{},"        \u003C\u002Fdiv>\n",[114,5723,5725],{"class":116,"line":5724},52,[114,5726,5727],{},"      ) : (\n",[114,5729,5731],{"class":116,"line":5730},53,[114,5732,5733],{},"        \u003CButton\n",[114,5735,5737],{"class":116,"line":5736},54,[114,5738,5739],{},"          onClick={handleResend}\n",[114,5741,5743],{"class":116,"line":5742},55,[114,5744,5745],{},"          className=\"w-full border border-blue-600 text-blue-600 bg-white\"\n",[114,5747,5749],{"class":116,"line":5748},56,[114,5750,5751],{},"          disabled={isActive || status === \"executing\"}\n",[114,5753,5755],{"class":116,"line":5754},57,[114,5756,5757],{},"        >\n",[114,5759,5761],{"class":116,"line":5760},58,[114,5762,5763],{},"          {isActive\n",[114,5765,5767],{"class":116,"line":5766},59,[114,5768,5769],{},"            ? `Wait ${countdown} seconds to resend`\n",[114,5771,5773],{"class":116,"line":5772},60,[114,5774,5775],{},"            : \"Request Verification\"}\n",[114,5777,5779],{"class":116,"line":5778},61,[114,5780,5781],{},"        \u003C\u002FButton>\n",[114,5783,5785],{"class":116,"line":5784},62,[114,5786,5787],{},"      )}\n",[114,5789,5791],{"class":116,"line":5790},63,[114,5792,5793],{},"    \u003C\u002F>\n",[114,5795,5797],{"class":116,"line":5796},64,[114,5798,4396],{},[114,5800,5802],{"class":116,"line":5801},65,[114,5803,245],{},[36,5805,5807],{"id":5806},"demo-video-see-it-in-action","Demo Video: See It In Action",[16,5809,5810,5811,5813],{},"Now that you've seen the code and understand how ",[584,5812,4671],{}," helps you protect your server actions, let's take a look at how it all comes together in real life. Watch the demo below to see how it works in the frontend — from sending a verification email, handling user interactions, to receiving real-time feedback.",[16,5815,5816],{},[19,5817],{"alt":109,"src":5818},"https:\u002F\u002Fcdn-images-1.medium.com\u002Fmax\u002F1024\u002F1*fby0ebHnHsZIwym21s0dow.gif",[16,5820,5821],{},[536,5822,5823],{},"Demo showing the verification action performed",[16,5825,5826],{},[19,5827],{"alt":109,"src":5828},"https:\u002F\u002Fcdn-images-1.medium.com\u002Fmax\u002F1024\u002F1*T4bYxWcbPAHn0F_d5FxzWQ.gif",[16,5830,5831],{},[536,5832,5833],{},"Demo showing the email verification process — in user mailbox",[36,5835,5837],{"id":5836},"final-thoughts-locking-in-like-you-mean-it","Final Thoughts: Locking In, Like You Mean It",[16,5839,5840],{},"So yeah, there you have it. From yapping without filters to finally learning how to pause, process, and speak with intention. Just like in life, the loudest server isn't the wisest. It's the one that knows when to hold back, when to validate, and when to act with purpose.",[16,5842,5843],{},"That's what next-safe-action gives you.",[16,5845,5846,5847,5850],{},"It's not just some fancy abstraction. It's a mindset shift. A way to make sure your server isn't just reacting, but ",[536,5848,5849],{},"thinking."," A way to build trust with your users because your app's not out here doing the most when nobody asked.",[16,5852,5853,5854,5857],{},"Whether it's sending verification emails or handling critical flows, you've now got the tools to move with confidence. You're no longer hoping your backend behaves — you ",[536,5855,5856],{},"know"," it will, because you locked it in.",[16,5859,5860,5861],{},"So next time you're building out that shiny new feature, remember:\nDon't just let your server yap.\nTeach it to listen. Teach it to think.\nAnd when it's ready — ",[584,5862,5863],{},"let it speak with purpose.",[16,5865,5866],{},[584,5867,5868],{},"Explore More, Tweak, Play:",[2639,5870,5871,5880,5888,5897],{},[720,5872,5873,5874,5879],{},"🛠 ",[83,5875,5878],{"href":5876,"rel":5877},"https:\u002F\u002Fnext-safe-action-playground.vercel.app\u002F",[87],"Next Safe Action Playground"," — Try it out in real time. Break it. Fix it. Repeat.",[720,5881,5882,5883,5887],{},"📘 ",[83,5884,5886],{"href":4669,"rel":5885},[87],"Official Docs"," — Everything you need to know, from setup to best practices.",[720,5889,5890,5891,5896],{},"🧠 ",[83,5892,5895],{"href":5893,"rel":5894},"https:\u002F\u002Fgithub.com\u002FTheEdoRan\u002Fnext-safe-action",[87],"GitHub"," — Dive into the code, contribute, or just peep how it works under the hood.",[720,5898,5899,5900,5905],{},"🔐 Real-world example: ",[83,5901,5904],{"href":5902,"rel":5903},"https:\u002F\u002Fiq.wiki\u002F",[87],"IQ Wiki"," — The world's largest crypto encyclopedia, running safe and locked in.",[16,5907,5908,5909],{},"Happy building.\nAnd remember, ",[584,5910,5911],{},"talk less, lock in more.",[968,5913,5914],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":109,"searchDepth":124,"depth":124,"links":5916},[5917,5918,5919,5925,5926],{"id":4627,"depth":124,"text":4628},{"id":4687,"depth":124,"text":4688},{"id":4768,"depth":124,"text":4769,"children":5920},[5921,5922,5923,5924],{"id":4775,"depth":154,"text":4776},{"id":4803,"depth":154,"text":4804},{"id":4842,"depth":154,"text":4843},{"id":5070,"depth":154,"text":5071},{"id":5806,"depth":124,"text":5807},{"id":5836,"depth":124,"text":5837},"https:\u002F\u002Fiamlope.medium.com\u002Fstop-yapping-lock-in-5e0a0673ab19","2025-04-25","Securing Next.js server actions with next-safe-action — typed input validation, Zod schemas, and clean error handling.",{},"\u002Fblog\u002Fstop-yapping-lock-in",{"title":4577,"description":5929},"blog\u002Fstop-yapping-lock-in","IBR-5a3TNSaobi6uPM-xVOXyfgJ34xLv8LVGPMKsldw",{"id":5936,"title":5937,"body":5938,"canonical":7130,"date":7131,"description":7132,"draft":995,"duration":992,"extension":996,"lang":992,"meta":7133,"navigation":196,"path":7134,"place":992,"placeLink":992,"readingTime":992,"seo":7135,"stem":7136,"__hash__":7137},"blog\u002Fblog\u002Fnuqs-because-urls-should-do-more.md","Nuqs: Because URLs Should Do More",{"type":8,"value":5939,"toc":7112},[5940,5944,5949,5952,5955,5958,5961,5966,5971,5976,5981,5986,5991,5996,5999,6002,6006,6009,6016,6022,6025,6031,6034,6037,6041,6044,6049,6056,6066,6073,6076,6087,6093,6097,6107,6110,6115,6122,6127,6131,6140,6147,6153,6156,6159,6164,6168,6171,6174,6178,6181,6221,6224,6228,6231,6234,6311,6314,6317,6321,6324,6327,6330,6333,6571,6575,6589,6597,6600,6603,6607,6610,6614,6617,6643,6647,6654,6775,6779,6782,6998,7001,7021,7025,7031,7037,7056,7071,7075,7082,7093,7103,7109],[11,5941,5943],{"id":5942},"move-beyond-local-state-go-beyond-plus-ultra","Move beyond local state — Go beyond, plus ultra.",[16,5945,5946],{},[19,5947],{"alt":109,"src":5948,"fetchPriority":23,"loading":24},"https:\u002F\u002Fcdn-images-1.medium.com\u002Fmax\u002F665\u002F1*KUe2LG5cA3cR80GodnXW8A.jpeg",[16,5950,5951],{},"Valentine's Day. I had planned it down to perfection. The right place, the right atmosphere, the right seat — because ambiance matters, and I wanted this night to be unforgettable. I took my time picking the best spot, ensuring that when we arrived, everything would be set.",[16,5953,5954],{},"The booking was seamless, no friction at all. In fact, I could have sworn it was the smoothest reservation experience I'd ever had — like a moment of rare perfection in a world of chaos. But then, the moment of truth arrived.",[16,5956,5957],{},"I stepped in, walked up confidently to my designated table… and someone else was already sitting there.",[16,5959,5960],{},"I approached the host.",[16,5962,5963],{},[536,5964,5965],{},"\"Excuse me, I had a reservation for this spot.\"",[16,5967,5968],{},[536,5969,5970],{},"\"Ah, yes. Unfortunately, it's already occupied.\"",[16,5972,5973],{},[536,5974,5975],{},"\"I don't understand. I made a reservation. Do you have my reservation?\"",[16,5977,5978],{},[536,5979,5980],{},"\"Yes, we do, but the table is taken.\"",[16,5982,5983],{},[536,5984,5985],{},"\"But the reservation keeps the table here? That's why you have the reservation!\"",[16,5987,5988],{},[536,5989,5990],{},"\"I understand, sir, but — \"",[16,5992,5993],{},[536,5994,5995],{},"\"No, I don't think you do! You see, you know how to take a reservation, but you don't know how to hold the reservation. And really, the holding is the most important part of the reservation. Anybody can just take them!\"",[16,5997,5998],{},"The host blinked. I sighed. My girlfriend looked at me, amused but also wondering if she should start looking up alternatives. I, on the other hand, felt like I was in a sitcom episode, living out the very definition of modern frustration.",[16,6000,6001],{},"But this isn't just about restaurants.",[36,6003,6005],{"id":6004},"the-lost-spots-of-everyday-life","The Lost Spots of Everyday Life",[16,6007,6008],{},"Think about it. You've been here before, maybe not in a restaurant, but in some other equally annoying scenario.",[16,6010,6011,6012,6015],{},"Ever asked a friend to save you a seat in class, only to come back and find someone else sitting there, and your friend just shrugs like, ",[536,6013,6014],{},"\"I tried, bro.\"","?",[16,6017,6018,6019],{},"Or maybe you've been in a long queue, patiently waiting for your turn, and then — just for a moment — you step away. Maybe to grab something, maybe just to stretch. And when you return, the person behind you has edged forward, pretending like they've been next all along. You try to reason with them, but they just stare at you blankly like, ",[536,6020,6021],{},"\"I didn't see you there.\"",[16,6023,6024],{},"Or the worst — boarding a bus or train, where you find the perfect seat by the window. You drop your bag on it, step out for a second, and by the time you return, someone is already comfortably seated like they paid for the spot in blood.",[16,6026,6027,6028],{},"These little moments of frustration add up. Because in all of them, there's one underlying expectation: ",[584,6029,6030],{},"something should have held your place.",[16,6032,6033],{},"But it didn't.",[16,6035,6036],{},"And if you think that's bad, let's talk about when this happens online.",[36,6038,6040],{"id":6039},"the-digital-version-of-losing-your-spot","The Digital Version of Losing Your Spot",[16,6042,6043],{},"You're browsing through an online store, carefully filtering for the perfect item. You've selected the color, size, and quantity. Maybe you even checked the reviews. And then — oh, a notification pops up. You leave for just a second, come back, and refresh the page.",[16,6045,6046],{},[584,6047,6048],{},"Boom. Everything is gone.",[16,6050,6051,6052,6055],{},"Or maybe you're halfway through filling out a long, annoying form. You get distracted, switch tabs, and when you return — ",[584,6053,6054],{},"everything is wiped."," The form has reset like you were never there.",[16,6057,6058,6059,6062,6063],{},"Or worse, you find a great article, read halfway, and want to share it with a friend. You copy the link, send it over, but when they open it — ",[584,6060,6061],{},"it doesn't show what you were looking at."," They're at the homepage, lost and confused, asking, ",[536,6064,6065],{},"\"What exactly did you want me to see?\"",[16,6067,6068,6069,6072],{},"It's like telling a friend, ",[536,6070,6071],{},"\"Meet me at that cool café,\""," only for them to show up at a random gas station because, for some reason, your directions didn't hold.",[16,6074,6075],{},"And this is where websites mess up.",[16,6077,6078,6079,6082,6083,6086],{},"Just like my reservation should have ensured my seat remained mine, and just like your place in a queue should be honored, URL states should ",[584,6080,6081],{},"preserve user context."," Whether it's form inputs, filters, pagination, scroll positions, or shared links, your app should know how to ",[584,6084,6085],{},"hold"," a state, not just take it.",[16,6088,6089,6090],{},"Because let's be honest — ",[584,6091,6092],{},"anybody can take them.",[36,6094,6096],{"id":6095},"fixing-the-reservation-problem-url-as-a-state-manager","Fixing the Reservation Problem: URL as a State Manager",[16,6098,6099,6100,6103,6104,449],{},"Imagine if reservations worked like URLs. Instead of relying on a fragile, easily-overwritten system, the restaurant would store all reservation details in a ",[536,6101,6102],{},"permanent, accessible"," way. No mix-ups, no surprises. Your table would be marked, ",[536,6105,6106],{},"and that information would persist",[16,6108,6109],{},"This is exactly how URLs should work in web applications. A well-managed URL state ensures that user context is never lost — whether it's search filters, form inputs, pagination, or even scroll position. When a user refreshes, switches devices, or shares a link, everything should stay exactly as they left it.",[16,6111,6112],{},[584,6113,6114],{},"So how do we fix the problem?",[16,6116,6117,6118,6121],{},"Instead of treating URLs as just links, we should treat them as ",[584,6119,6120],{},"state managers"," — a single source of truth that keeps track of where users are and what they're doing.",[16,6123,4703,6124,4706],{},[536,6125,6126],{},"nuqs",[36,6128,6130],{"id":6129},"nuqs-making-url-state-management-effortless","Nuqs: Making URL State Management Effortless",[16,6132,6133,6134,6136,6137,6139],{},"One of my favorite tools to use is ",[584,6135,6126],{},". I first discovered ",[536,6138,6126],{}," last year while revamping a project, and it instantly clicked. It felt like using useState, but for URLs.",[16,6141,6142,6143,6146],{},"It was seamless — like a reservation system that actually works. No weird resets, no lost state, just smooth navigation. It integrates beautifully with both server and client environments, and with the rise of ",[584,6144,6145],{},"SSR",", it's a game-changer.",[16,6148,6149,6150,6015],{},"And if you're using ",[584,6151,6152],{},"Next.js",[16,6154,6155],{},"Well, let's just say you've got yourself the perfect date.",[16,6157,6158],{},"So, whether it's a dinner reservation, a classroom seat, or your place in a digital space, remember this:",[16,6160,6161],{},[584,6162,6163],{},"Taking a spot is easy. Holding it is what truly matters.",[36,6165,6167],{"id":6166},"setting-up-nuqs-keeping-your-spot-in-the-digital-world","Setting Up Nuqs: Keeping Your Spot in the Digital World",[16,6169,6170],{},"A reservation system that actually works — that's what I needed. Not just for dinner, but for my apps. No more lost filters, wiped-out forms, or reset states. If I left something exactly the way I wanted it, I expected it to stay that way.",[16,6172,6173],{},"That's where Nuqs came in. But before I could fully enjoy its magic, I had to set it up properly. Just like how you don't walk into a restaurant and expect a table to set itself, you need to prepare your app to hold state effortlessly.",[36,6175,6177],{"id":6176},"step-1-installing-nuqs","Step 1: Installing Nuqs",[16,6179,6180],{},"The first step was making sure Nuqs was in my project. Installing it was as easy as reserving a spot with a single click:",[104,6182,6184],{"className":1309,"code":6183,"language":1311,"meta":109,"style":109},"pnpm add nuqs\n# or\nnpm install nuqs\n# or\nyarn add nuqs\n",[111,6185,6186,6195,6200,6208,6212],{"__ignoreMap":109},[114,6187,6188,6190,6192],{"class":116,"line":117},[114,6189,4792],{"class":212},[114,6191,1328],{"class":1321},[114,6193,6194],{"class":1321}," nuqs\n",[114,6196,6197],{"class":116,"line":124},[114,6198,6199],{"class":120},"# or\n",[114,6201,6202,6204,6206],{"class":116,"line":154},[114,6203,3571],{"class":212},[114,6205,3574],{"class":1321},[114,6207,6194],{"class":1321},[114,6209,6210],{"class":116,"line":174},[114,6211,6199],{"class":120},[114,6213,6214,6217,6219],{"class":116,"line":193},[114,6215,6216],{"class":212},"yarn",[114,6218,1328],{"class":1321},[114,6220,6194],{"class":1321},[16,6222,6223],{},"With that done, I was one step closer to never losing my place again.",[36,6225,6227],{"id":6226},"step-2-configuring-nuqs-in-the-root-layout","Step 2: Configuring Nuqs in the Root Layout",[16,6229,6230],{},"Just like a restaurant needs a reservation system in place, my app needed Nuqs to be present across all pages. The best way to do that? Wrap the entire application in a provider that ensures state persistence across routes.",[16,6232,6233],{},"In my app.tsx, I set up Nuqs like this:",[104,6235,6237],{"className":1193,"code":6236,"language":1195,"meta":109,"style":109},"import { NuqsAdapter } from 'nuqs\u002Fadapters\u002Fnext\u002Fapp'\nimport { type ReactNode } from 'react'\n\nexport default function RootLayout({\n  children\n}: {\n  children: ReactNode\n}) {\n  return (\n    \u003Chtml>\n      \u003Cbody>\n        \u003CNuqsAdapter>{children}\u003C\u002FNuqsAdapter>\n      \u003C\u002Fbody>\n    \u003C\u002Fhtml>\n  )\n}\n",[111,6238,6239,6244,6249,6253,6258,6263,6267,6272,6276,6280,6285,6289,6294,6298,6302,6307],{"__ignoreMap":109},[114,6240,6241],{"class":116,"line":117},[114,6242,6243],{},"import { NuqsAdapter } from 'nuqs\u002Fadapters\u002Fnext\u002Fapp'\n",[114,6245,6246],{"class":116,"line":124},[114,6247,6248],{},"import { type ReactNode } from 'react'\n",[114,6250,6251],{"class":116,"line":154},[114,6252,197],{"emptyLinePlaceholder":196},[114,6254,6255],{"class":116,"line":174},[114,6256,6257],{},"export default function RootLayout({\n",[114,6259,6260],{"class":116,"line":193},[114,6261,6262],{},"  children\n",[114,6264,6265],{"class":116,"line":200},[114,6266,4311],{},[114,6268,6269],{"class":116,"line":206},[114,6270,6271],{},"  children: ReactNode\n",[114,6273,6274],{"class":116,"line":219},[114,6275,4326],{},[114,6277,6278],{"class":116,"line":228},[114,6279,4360],{},[114,6281,6282],{"class":116,"line":236},[114,6283,6284],{},"    \u003Chtml>\n",[114,6286,6287],{"class":116,"line":242},[114,6288,4372],{},[114,6290,6291],{"class":116,"line":248},[114,6292,6293],{},"        \u003CNuqsAdapter>{children}\u003C\u002FNuqsAdapter>\n",[114,6295,6296],{"class":116,"line":253},[114,6297,4384],{},[114,6299,6300],{"class":116,"line":401},[114,6301,4390],{},[114,6303,6304],{"class":116,"line":407},[114,6305,6306],{},"  )\n",[114,6308,6309],{"class":116,"line":412},[114,6310,440],{},[16,6312,6313],{},"This was like having the maître d' of my app, making sure everything stayed exactly where it was supposed to.",[16,6315,6316],{},"And just like that, my application was ready. But what good is a reservation system if you don't use it? It was time to put Nuqs into action.",[36,6318,6320],{"id":6319},"basic-usage-the-counter-that-remembers","Basic Usage: The Counter That Remembers",[16,6322,6323],{},"Now, let's talk about an everyday situation where Nuqs shines.",[16,6325,6326],{},"Imagine you're counting something important — maybe reps at the gym, the number of coffees you've had today, or how many times you've reloaded Twitter. Now imagine refreshing the page and losing count. Frustrating, right?",[16,6328,6329],{},"That's where Nuqs steps in.",[16,6331,6332],{},"Take a look at this simple counter demo.",[104,6334,6336],{"className":1193,"code":6335,"language":1195,"meta":109,"style":109},"\u002F\u002F client.tsx\n\n'use client';\n\nimport { Button } from '@\u002Fcomponents\u002Fui\u002Fbutton';\nimport { Minus, Plus } from 'lucide-react';\nimport { parseAsInteger, useQueryState } from 'nuqs';\n\nexport default function BasicCounterDemoPage() {\n  const [counter, setCounter] = useQueryState(\n    'counter',\n    parseAsInteger.withDefault(0)\n  );\n\n  return (\n    \u003Cdiv className='min-h-screen flex items-center justify-center bg-gradient-to-r from-blue-50 to-indigo-50'>\n      \u003Cdiv className='bg-white p-8 rounded-xl shadow-lg'>\n        \u003Ch1 className='text-3xl font-bold text-gray-800 mb-6 text-center'>\n          Interactive Counter\n        \u003C\u002Fh1>\n        \u003Cdiv className='flex flex-col items-center gap-6'>\n          \u003Cspan className='text-4xl font-bold text-indigo-600 tabular-nums'>\n            {counter}\n          \u003C\u002Fspan>\n          \u003Cdiv className='flex items-center gap-4'>\n            \u003CButton\n              onClick={() => setCounter((x) => Math.max(1, x - 1))}\n              className='bg-indigo-600 hover:bg-indigo-700 transition-colors'\n            >\n              \u003CMinus className='w-5 h-5' \u002F>\n            \u003C\u002FButton>\n            \u003CButton\n              onClick={() => setCounter((x) => x + 1)}\n              className='bg-indigo-600 hover:bg-indigo-700 transition-colors'\n            >\n              \u003CPlus className='w-5 h-5' \u002F>\n            \u003C\u002FButton>\n          \u003C\u002Fdiv>\n          \u003CButton\n            onClick={() => setCounter(null)}\n            variant='outline'\n            className='text-indigo-600 border-indigo-600 hover:bg-indigo-50'\n          >\n            Reset Counter\n          \u003C\u002FButton>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  );\n}\n",[111,6337,6338,6343,6347,6352,6356,6361,6366,6371,6375,6380,6385,6390,6395,6399,6403,6407,6412,6417,6422,6427,6432,6437,6442,6447,6452,6457,6462,6467,6472,6477,6482,6487,6491,6496,6500,6504,6509,6513,6518,6522,6527,6532,6537,6541,6546,6550,6554,6559,6563,6567],{"__ignoreMap":109},[114,6339,6340],{"class":116,"line":117},[114,6341,6342],{},"\u002F\u002F client.tsx\n",[114,6344,6345],{"class":116,"line":124},[114,6346,197],{"emptyLinePlaceholder":196},[114,6348,6349],{"class":116,"line":154},[114,6350,6351],{},"'use client';\n",[114,6353,6354],{"class":116,"line":174},[114,6355,197],{"emptyLinePlaceholder":196},[114,6357,6358],{"class":116,"line":193},[114,6359,6360],{},"import { Button } from '@\u002Fcomponents\u002Fui\u002Fbutton';\n",[114,6362,6363],{"class":116,"line":200},[114,6364,6365],{},"import { Minus, Plus } from 'lucide-react';\n",[114,6367,6368],{"class":116,"line":206},[114,6369,6370],{},"import { parseAsInteger, useQueryState } from 'nuqs';\n",[114,6372,6373],{"class":116,"line":219},[114,6374,197],{"emptyLinePlaceholder":196},[114,6376,6377],{"class":116,"line":228},[114,6378,6379],{},"export default function BasicCounterDemoPage() {\n",[114,6381,6382],{"class":116,"line":236},[114,6383,6384],{},"  const [counter, setCounter] = useQueryState(\n",[114,6386,6387],{"class":116,"line":242},[114,6388,6389],{},"    'counter',\n",[114,6391,6392],{"class":116,"line":248},[114,6393,6394],{},"    parseAsInteger.withDefault(0)\n",[114,6396,6397],{"class":116,"line":253},[114,6398,4396],{},[114,6400,6401],{"class":116,"line":401},[114,6402,197],{"emptyLinePlaceholder":196},[114,6404,6405],{"class":116,"line":407},[114,6406,4360],{},[114,6408,6409],{"class":116,"line":412},[114,6410,6411],{},"    \u003Cdiv className='min-h-screen flex items-center justify-center bg-gradient-to-r from-blue-50 to-indigo-50'>\n",[114,6413,6414],{"class":116,"line":437},[114,6415,6416],{},"      \u003Cdiv className='bg-white p-8 rounded-xl shadow-lg'>\n",[114,6418,6419],{"class":116,"line":4357},[114,6420,6421],{},"        \u003Ch1 className='text-3xl font-bold text-gray-800 mb-6 text-center'>\n",[114,6423,6424],{"class":116,"line":4363},[114,6425,6426],{},"          Interactive Counter\n",[114,6428,6429],{"class":116,"line":4369},[114,6430,6431],{},"        \u003C\u002Fh1>\n",[114,6433,6434],{"class":116,"line":4375},[114,6435,6436],{},"        \u003Cdiv className='flex flex-col items-center gap-6'>\n",[114,6438,6439],{"class":116,"line":4381},[114,6440,6441],{},"          \u003Cspan className='text-4xl font-bold text-indigo-600 tabular-nums'>\n",[114,6443,6444],{"class":116,"line":4387},[114,6445,6446],{},"            {counter}\n",[114,6448,6449],{"class":116,"line":4393},[114,6450,6451],{},"          \u003C\u002Fspan>\n",[114,6453,6454],{"class":116,"line":4399},[114,6455,6456],{},"          \u003Cdiv className='flex items-center gap-4'>\n",[114,6458,6459],{"class":116,"line":5340},[114,6460,6461],{},"            \u003CButton\n",[114,6463,6464],{"class":116,"line":5352},[114,6465,6466],{},"              onClick={() => setCounter((x) => Math.max(1, x - 1))}\n",[114,6468,6469],{"class":116,"line":5372},[114,6470,6471],{},"              className='bg-indigo-600 hover:bg-indigo-700 transition-colors'\n",[114,6473,6474],{"class":116,"line":5385},[114,6475,6476],{},"            >\n",[114,6478,6479],{"class":116,"line":5391},[114,6480,6481],{},"              \u003CMinus className='w-5 h-5' \u002F>\n",[114,6483,6484],{"class":116,"line":5396},[114,6485,6486],{},"            \u003C\u002FButton>\n",[114,6488,6489],{"class":116,"line":5413},[114,6490,6461],{},[114,6492,6493],{"class":116,"line":5418},[114,6494,6495],{},"              onClick={() => setCounter((x) => x + 1)}\n",[114,6497,6498],{"class":116,"line":5618},[114,6499,6471],{},[114,6501,6502],{"class":116,"line":5624},[114,6503,6476],{},[114,6505,6506],{"class":116,"line":5629},[114,6507,6508],{},"              \u003CPlus className='w-5 h-5' \u002F>\n",[114,6510,6511],{"class":116,"line":5634},[114,6512,6486],{},[114,6514,6515],{"class":116,"line":5640},[114,6516,6517],{},"          \u003C\u002Fdiv>\n",[114,6519,6520],{"class":116,"line":5646},[114,6521,5661],{},[114,6523,6524],{"class":116,"line":5652},[114,6525,6526],{},"            onClick={() => setCounter(null)}\n",[114,6528,6529],{"class":116,"line":5658},[114,6530,6531],{},"            variant='outline'\n",[114,6533,6534],{"class":116,"line":5664},[114,6535,6536],{},"            className='text-indigo-600 border-indigo-600 hover:bg-indigo-50'\n",[114,6538,6539],{"class":116,"line":5670},[114,6540,5691],{},[114,6542,6543],{"class":116,"line":5676},[114,6544,6545],{},"            Reset Counter\n",[114,6547,6548],{"class":116,"line":5682},[114,6549,5715],{},[114,6551,6552],{"class":116,"line":5688},[114,6553,5721],{},[114,6555,6556],{"class":116,"line":5694},[114,6557,6558],{},"      \u003C\u002Fdiv>\n",[114,6560,6561],{"class":116,"line":5700},[114,6562,4466],{},[114,6564,6565],{"class":116,"line":5706},[114,6566,4396],{},[114,6568,6569],{"class":116,"line":5712},[114,6570,440],{},[36,6572,6574],{"id":6573},"how-it-works","How It Works",[2639,6576,6577,6580,6583,6586],{},[720,6578,6579],{},"The counter state is stored in the URL as ?counter=number.",[720,6581,6582],{},"Clicking Plus (+) or Minus (-) updates the value while keeping it in sync.",[720,6584,6585],{},"Clicking Reset removes the state from the URL.",[720,6587,6588],{},"Even if the user refreshes the page or shares the link, the counter value remains intact.",[16,6590,6591,6594],{},[19,6592],{"alt":109,"src":6593},"https:\u002F\u002Fcdn-images-1.medium.com\u002Fmax\u002F1024\u002F1*-7t31FDkCiVe9_fFN723tQ.gif",[536,6595,6596],{},"Nuqs Counter Demo",[16,6598,6599],{},"It's like saving your seat and coming back to find it exactly where you left it. No mix-ups. No \"Sorry, we gave it away.\" Just seamless, reliable state persistence.",[16,6601,6602],{},"With Nuqs, my app wasn't just taking reservations — it was holding them. And that made all the difference.",[36,6604,6606],{"id":6605},"building-a-comprehensive-table","Building a Comprehensive Table",[16,6608,6609],{},"Reserving a table is great, but what if you need a whole VIP section? In a dynamic app, handling more complex state persistence becomes crucial. That's where an advanced table setup comes in.",[36,6611,6613],{"id":6612},"advanced-tables-where-nuqs-shines","Advanced Tables: Where Nuqs Shines",[16,6615,6616],{},"A table is a great next step because it introduces:",[2639,6618,6619,6625,6631,6637],{},[720,6620,6621,6624],{},[584,6622,6623],{},"Sorting"," — Allow users to order data dynamically",[720,6626,6627,6630],{},[584,6628,6629],{},"Filtering"," — Let users refine what they see",[720,6632,6633,6636],{},[584,6634,6635],{},"Pagination"," — Efficiently manage large datasets",[720,6638,6639,6642],{},[584,6640,6641],{},"State Persistence"," — Keep everything in sync via the URL",[36,6644,6646],{"id":6645},"setting-up-the-server-action","Setting Up the Server Action",[16,6648,6649,6650,6653],{},"First, let's create a ",[584,6651,6652],{},"server action"," to fetch data based on query parameters:",[104,6655,6657],{"className":1954,"code":6656,"language":1956,"meta":109,"style":109},"'use server'\n\nimport { getDatabaseResults } from '@\u002Fsrc\u002Flib\u002Fdb'\n\nexport async function fetchTableData({ page, sort, filter }: {\n  page?: number;\n  sort?: string;\n  filter?: string;\n}) {\n  return await getDatabaseResults({ page, sort, filter });\n}\n",[111,6658,6659,6664,6668,6680,6684,6720,6732,6744,6755,6759,6771],{"__ignoreMap":109},[114,6660,6661],{"class":116,"line":117},[114,6662,6663],{"class":1321},"'use server'\n",[114,6665,6666],{"class":116,"line":124},[114,6667,197],{"emptyLinePlaceholder":196},[114,6669,6670,6672,6675,6677],{"class":116,"line":154},[114,6671,3676],{"class":127},[114,6673,6674],{"class":137}," { getDatabaseResults } ",[114,6676,2350],{"class":127},[114,6678,6679],{"class":1321}," '@\u002Fsrc\u002Flib\u002Fdb'\n",[114,6681,6682],{"class":116,"line":174},[114,6683,197],{"emptyLinePlaceholder":196},[114,6685,6686,6688,6691,6694,6697,6700,6703,6705,6708,6710,6713,6716,6718],{"class":116,"line":193},[114,6687,3741],{"class":127},[114,6689,6690],{"class":127}," async",[114,6692,6693],{"class":127}," function",[114,6695,6696],{"class":212}," fetchTableData",[114,6698,6699],{"class":137},"({ ",[114,6701,6702],{"class":2361},"page",[114,6704,1085],{"class":137},[114,6706,6707],{"class":2361},"sort",[114,6709,1085],{"class":137},[114,6711,6712],{"class":2361},"filter",[114,6714,6715],{"class":137}," }",[114,6717,1987],{"class":127},[114,6719,216],{"class":137},[114,6721,6722,6725,6727,6730],{"class":116,"line":200},[114,6723,6724],{"class":2361},"  page",[114,6726,4935],{"class":127},[114,6728,6729],{"class":144}," number",[114,6731,171],{"class":137},[114,6733,6734,6737,6739,6742],{"class":116,"line":206},[114,6735,6736],{"class":2361},"  sort",[114,6738,4935],{"class":127},[114,6740,6741],{"class":144}," string",[114,6743,171],{"class":137},[114,6745,6746,6749,6751,6753],{"class":116,"line":219},[114,6747,6748],{"class":2361},"  filter",[114,6750,4935],{"class":127},[114,6752,6741],{"class":144},[114,6754,171],{"class":137},[114,6756,6757],{"class":116,"line":228},[114,6758,4326],{"class":137},[114,6760,6761,6763,6765,6768],{"class":116,"line":236},[114,6762,415],{"class":127},[114,6764,2232],{"class":127},[114,6766,6767],{"class":212}," getDatabaseResults",[114,6769,6770],{"class":137},"({ page, sort, filter });\n",[114,6772,6773],{"class":116,"line":242},[114,6774,440],{"class":137},[36,6776,6778],{"id":6777},"client-table-component","Client Table Component",[16,6780,6781],{},"Now, let's create a client-side table that syncs with the URL using Nuqs:",[104,6783,6785],{"className":1193,"code":6784,"language":1195,"meta":109,"style":109},"'use client'\n\nimport { useQueryState, parseAsString, parseAsInteger } from 'nuqs'\nimport { fetchTableData } from '@\u002Fsrc\u002Factions\u002FtableActions'\n\nexport default function AdvancedTable() {\n  const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))\n  const [sort, setSort] = useQueryState('sort', parseAsString)\n  const [filter, setFilter] = useQueryState('filter', parseAsString)\n\n  const { data, error } = use(fetchTableData({ page, sort, filter }))\n\n  return (\n    \u003Cdiv>\n      \u003Cinput\n        type=\"text\"\n        placeholder=\"Filter...\"\n        value={filter ?? ''}\n        onChange={(e) => setFilter(e.target.value || null)}\n      \u002F>\n      \u003Cbutton onClick={() => setSort(sort === 'asc' ? 'desc' : 'asc')}>\n        Sort: {sort === 'asc' ? 'Descending' : 'Ascending'}\n      \u003C\u002Fbutton>\n      \u003Ctable>\n        \u003Cthead>\n          \u003Ctr>\n            \u003Cth>Name\u003C\u002Fth>\n            \u003Cth>Value\u003C\u002Fth>\n          \u003C\u002Ftr>\n        \u003C\u002Fthead>\n        \u003Ctbody>\n          {data?.map((item) => (\n            \u003Ctr key={item.id}>\n              \u003Ctd>{item.name}\u003C\u002Ftd>\n              \u003Ctd>{item.value}\u003C\u002Ftd>\n            \u003C\u002Ftr>\n          ))}\n        \u003C\u002Ftbody>\n      \u003C\u002Ftable>\n      \u003Cbutton onClick={() => setPage(page - 1)} disabled={page \u003C= 1}>Previous\u003C\u002Fbutton>\n      \u003Cbutton onClick={() => setPage(page + 1)}>Next\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  )\n}\n",[111,6786,6787,6792,6796,6801,6806,6810,6815,6820,6825,6830,6834,6839,6843,6847,6851,6856,6861,6866,6871,6876,6881,6886,6891,6896,6901,6906,6911,6916,6921,6926,6931,6936,6941,6946,6951,6956,6961,6966,6971,6976,6981,6986,6990,6994],{"__ignoreMap":109},[114,6788,6789],{"class":116,"line":117},[114,6790,6791],{},"'use client'\n",[114,6793,6794],{"class":116,"line":124},[114,6795,197],{"emptyLinePlaceholder":196},[114,6797,6798],{"class":116,"line":154},[114,6799,6800],{},"import { useQueryState, parseAsString, parseAsInteger } from 'nuqs'\n",[114,6802,6803],{"class":116,"line":174},[114,6804,6805],{},"import { fetchTableData } from '@\u002Fsrc\u002Factions\u002FtableActions'\n",[114,6807,6808],{"class":116,"line":193},[114,6809,197],{"emptyLinePlaceholder":196},[114,6811,6812],{"class":116,"line":200},[114,6813,6814],{},"export default function AdvancedTable() {\n",[114,6816,6817],{"class":116,"line":206},[114,6818,6819],{},"  const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))\n",[114,6821,6822],{"class":116,"line":219},[114,6823,6824],{},"  const [sort, setSort] = useQueryState('sort', parseAsString)\n",[114,6826,6827],{"class":116,"line":228},[114,6828,6829],{},"  const [filter, setFilter] = useQueryState('filter', parseAsString)\n",[114,6831,6832],{"class":116,"line":236},[114,6833,197],{"emptyLinePlaceholder":196},[114,6835,6836],{"class":116,"line":242},[114,6837,6838],{},"  const { data, error } = use(fetchTableData({ page, sort, filter }))\n",[114,6840,6841],{"class":116,"line":248},[114,6842,197],{"emptyLinePlaceholder":196},[114,6844,6845],{"class":116,"line":253},[114,6846,4360],{},[114,6848,6849],{"class":116,"line":401},[114,6850,4451],{},[114,6852,6853],{"class":116,"line":407},[114,6854,6855],{},"      \u003Cinput\n",[114,6857,6858],{"class":116,"line":412},[114,6859,6860],{},"        type=\"text\"\n",[114,6862,6863],{"class":116,"line":437},[114,6864,6865],{},"        placeholder=\"Filter...\"\n",[114,6867,6868],{"class":116,"line":4357},[114,6869,6870],{},"        value={filter ?? ''}\n",[114,6872,6873],{"class":116,"line":4363},[114,6874,6875],{},"        onChange={(e) => setFilter(e.target.value || null)}\n",[114,6877,6878],{"class":116,"line":4369},[114,6879,6880],{},"      \u002F>\n",[114,6882,6883],{"class":116,"line":4375},[114,6884,6885],{},"      \u003Cbutton onClick={() => setSort(sort === 'asc' ? 'desc' : 'asc')}>\n",[114,6887,6888],{"class":116,"line":4381},[114,6889,6890],{},"        Sort: {sort === 'asc' ? 'Descending' : 'Ascending'}\n",[114,6892,6893],{"class":116,"line":4387},[114,6894,6895],{},"      \u003C\u002Fbutton>\n",[114,6897,6898],{"class":116,"line":4393},[114,6899,6900],{},"      \u003Ctable>\n",[114,6902,6903],{"class":116,"line":4399},[114,6904,6905],{},"        \u003Cthead>\n",[114,6907,6908],{"class":116,"line":5340},[114,6909,6910],{},"          \u003Ctr>\n",[114,6912,6913],{"class":116,"line":5352},[114,6914,6915],{},"            \u003Cth>Name\u003C\u002Fth>\n",[114,6917,6918],{"class":116,"line":5372},[114,6919,6920],{},"            \u003Cth>Value\u003C\u002Fth>\n",[114,6922,6923],{"class":116,"line":5385},[114,6924,6925],{},"          \u003C\u002Ftr>\n",[114,6927,6928],{"class":116,"line":5391},[114,6929,6930],{},"        \u003C\u002Fthead>\n",[114,6932,6933],{"class":116,"line":5396},[114,6934,6935],{},"        \u003Ctbody>\n",[114,6937,6938],{"class":116,"line":5413},[114,6939,6940],{},"          {data?.map((item) => (\n",[114,6942,6943],{"class":116,"line":5418},[114,6944,6945],{},"            \u003Ctr key={item.id}>\n",[114,6947,6948],{"class":116,"line":5618},[114,6949,6950],{},"              \u003Ctd>{item.name}\u003C\u002Ftd>\n",[114,6952,6953],{"class":116,"line":5624},[114,6954,6955],{},"              \u003Ctd>{item.value}\u003C\u002Ftd>\n",[114,6957,6958],{"class":116,"line":5629},[114,6959,6960],{},"            \u003C\u002Ftr>\n",[114,6962,6963],{"class":116,"line":5634},[114,6964,6965],{},"          ))}\n",[114,6967,6968],{"class":116,"line":5640},[114,6969,6970],{},"        \u003C\u002Ftbody>\n",[114,6972,6973],{"class":116,"line":5646},[114,6974,6975],{},"      \u003C\u002Ftable>\n",[114,6977,6978],{"class":116,"line":5652},[114,6979,6980],{},"      \u003Cbutton onClick={() => setPage(page - 1)} disabled={page \u003C= 1}>Previous\u003C\u002Fbutton>\n",[114,6982,6983],{"class":116,"line":5658},[114,6984,6985],{},"      \u003Cbutton onClick={() => setPage(page + 1)}>Next\u003C\u002Fbutton>\n",[114,6987,6988],{"class":116,"line":5664},[114,6989,4466],{},[114,6991,6992],{"class":116,"line":5670},[114,6993,6306],{},[114,6995,6996],{"class":116,"line":5676},[114,6997,440],{},[16,6999,7000],{},"This ensures:",[2639,7002,7003,7009,7015],{},[720,7004,7005,7006],{},"URL-based ",[584,7007,7008],{},"pagination, sorting, and filtering",[720,7010,7011,7014],{},[584,7012,7013],{},"State persistence"," across refreshes and navigation",[720,7016,7017,7020],{},[584,7018,7019],{},"Seamless user experience"," with Nuqs managing query states",[36,7022,7024],{"id":7023},"final-thoughts-holding-the-spot-that-matters","Final Thoughts: Holding the Spot That Matters",[16,7026,7027,7028,449],{},"At the end of the day, whether it's a table at a restaurant, a seat in a lecture hall, or your place in a digital experience, what we really want is simple: ",[584,7029,7030],{},"to not lose our spot",[16,7032,7033,7034,449],{},"Just like a well-managed reservation system ensures your table stays yours, a well-structured URL state ensures that users never feel lost or reset. It's not just about good UX — it's about ",[584,7035,7036],{},"respecting user intent",[16,7038,7039,7040,7043,7044,7047,7048,7051,7052,7055],{},"That's where ",[584,7041,7042],{},"Nuqs"," stands out. Unlike useSearchParams, which is limited to basic query strings, or ",[584,7045,7046],{},"state managers like Zustand and Redux",", which primarily handle ",[584,7049,7050],{},"in-app state"," (and lose it on refresh unless you introduce a persistence layer like localStorage), Nuqs ensures that state persists ",[584,7053,7054],{},"naturally via the URL","—without extra setup.",[16,7057,7058,7059,7062,7063,7066,7067,7070],{},"Beyond just improving user experience, ",[584,7060,7061],{},"persisting state in URLs has real advantages"," — it makes content more ",[584,7064,7065],{},"SEO-friendly",", allowing users to share stateful URLs effortlessly. It also enhances ",[584,7068,7069],{},"accessibility",", ensuring users returning via bookmarks or assistive technologies don't start from scratch.",[36,7072,7074],{"id":7073},"a-quick-note-on-limits","🚦 A Quick Note on Limits",[16,7076,7077,7078,7081],{},"Of course, even the best reservation systems have their limits — there's only so much space in a restaurant, and only so many seats in a theater. URLs are the same. While Nuqs lets you hold your spot seamlessly, ",[584,7079,7080],{},"every browser has a max URL length"," — and just like a restaurant that can't fit an entire wedding party at one table, a URL isn't the place for storing massive application state.",[16,7083,7084,7085,7088,7089,7092],{},"And just like some venues might have a \"no back-to-back reservations\" policy, browsers also ",[584,7086,7087],{},"throttle rapid URL updates"," to ensure smooth performance. Luckily, Nuqs ",[584,7090,7091],{},"handles this for you",", so you don't have to worry about overloading the system.",[16,7094,7095,7096,7099,7100],{},"So, as always, ",[584,7097,7098],{},"balance is key",". Not every piece of state belongs in the URL — ",[584,7101,7102],{},"but when it does, Nuqs makes sure it stays there.",[16,7104,7105,7106],{},"Because at the end of the day, the best experiences are the ones that let you leave — and come back — ",[584,7107,7108],{},"without losing your place.",[968,7110,7111],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":109,"searchDepth":124,"depth":124,"links":7113},[7114,7115,7116,7117,7118,7119,7120,7121,7122,7123,7124,7125,7126,7127,7128,7129],{"id":5942,"depth":154,"text":5943},{"id":6004,"depth":124,"text":6005},{"id":6039,"depth":124,"text":6040},{"id":6095,"depth":124,"text":6096},{"id":6129,"depth":124,"text":6130},{"id":6166,"depth":124,"text":6167},{"id":6176,"depth":124,"text":6177},{"id":6226,"depth":124,"text":6227},{"id":6319,"depth":124,"text":6320},{"id":6573,"depth":124,"text":6574},{"id":6605,"depth":124,"text":6606},{"id":6612,"depth":124,"text":6613},{"id":6645,"depth":124,"text":6646},{"id":6777,"depth":124,"text":6778},{"id":7023,"depth":124,"text":7024},{"id":7073,"depth":124,"text":7074},"https:\u002F\u002Fiamlope.medium.com\u002Fnuqs-because-urls-should-do-more-5d5d86e873c1","2025-02-17","Type-safe URL search-param state management in Next.js with nuqs, so user context persists across refreshes and shared links.",{},"\u002Fblog\u002Fnuqs-because-urls-should-do-more",{"title":5937,"description":7132},"blog\u002Fnuqs-because-urls-should-do-more","kHLv2ArDYFFJUxtg1R9HU_Cuuzc4cFj1exbeED94t6k",1788296147741]