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