[{"data":1,"prerenderedAt":1418},["ShallowReactive",2],{"home":3,"projects-latest":46,"posts-latest":77},{"id":4,"title":5,"availability":6,"body":7,"description":37,"extension":38,"location":39,"meta":40,"navigation":41,"path":42,"seo":43,"stem":44,"__hash__":45},"home\u002Findex.md","Hassan Qasemi","Open to interesting collaborations",{"type":8,"value":9,"toc":31},"minimark",[10,14,17,22,25,28],[11,12,13],"p",{},"I’m Hassan and this is my corner of the internet, Welcome!",[11,15,16],{},"I’m a software engineer and kinda web developer who enjoys building things that feel considered: from programming languages to toolchains, full-fledged websites that run like butter, focused tools, and command-line utilities that get the job done without unnecessary complexity.",[18,19,21],"h2",{"id":20},"what-i-care-about","What I care about",[11,23,24],{},"I'm mostly focused on system tools, low-level concepts, and programming languages.",[11,26,27],{},"I like software with a clear point of view. My work usually sits somewhere between product interface and system detail, with an emphasis on clean code, good performance, and experiences that stay out of the way.",[11,29,30],{},"Beyond code, I’m documenting how I think and build: from deep dives into Rust to Nuxt project walkthroughs. I’m interested in calm, project-driven programming across different operating systems, and in sharing the process with people who care about the craft.",{"title":32,"searchDepth":33,"depth":34,"links":35},"",2,3,[36],{"id":20,"depth":33,"text":21},"Software engineer and semi web developer trying to building thoughtful software, useful tools, and calm digital experiences.","md",null,{},true,"\u002F",{"title":5,"description":37},"index","f6F4s3nuhIDSkr9gp7FoU_eJijp62MyvHpyANMhBT1g",[47,58,68],{"id":48,"title":49,"date":50,"description":51,"extension":52,"meta":53,"stack":54,"stem":55,"story":39,"url":56,"__hash__":57},"projects\u002Fprojects\u002Fhella.yml","hella","2026-09-07","A modern systems language for curious minds","yml",{},"Rust","projects\u002Fhella","https:\u002F\u002Fgithub.com\u002Fhella-lang\u002Fhella","ZKAAm8IORtJocFXipe2OPxlixjFDRvQjFH0_b-lcI3o",{"id":59,"title":60,"date":61,"description":62,"extension":52,"meta":63,"stack":64,"stem":65,"story":39,"url":66,"__hash__":67},"projects\u002Fprojects\u002Fmimir.yml","mimir","2026-09-03","Odin's little toolchain",{},"Odin","projects\u002Fmimir","https:\u002F\u002Fgithub.com\u002Frivethorn\u002Fmimir","VXIC5Sqqn-Ozmq8jBYGIaBNYT4eD7FmbGa-jmhMhdis",{"id":69,"title":70,"date":71,"description":72,"extension":52,"meta":73,"stack":64,"stem":74,"story":39,"url":75,"__hash__":76},"projects\u002Fprojects\u002Freflags.yml","reflags","2026-09-02","Builder-style command-line argument parser for Odin",{},"projects\u002Freflags","https:\u002F\u002Fgithub.com\u002Frivethorn\u002Freflags","_QwsGOdR6Y1vvbmzDwOAP2Ao05QbyUdjEcnx7HTQsjQ",[78,549,959],{"id":79,"title":80,"body":81,"date":540,"description":541,"extension":38,"meta":542,"navigation":41,"path":543,"seo":544,"stem":545,"tag":546,"ttr":547,"__hash__":548},"writing\u002Fwritings\u002Fwe-need-to-slow-down.md","We Need to Slow Down",{"type":8,"value":82,"toc":527},[83,86,89,92,95,98,101,104,108,111,114,130,133,136,139,142,144,148,151,164,167,170,173,178,181,184,198,201,204,207,210,213,217,220,223,226,237,240,243,246,248,252,255,258,261,264,267,270,278,281,284,287,290,307,310,313,315,319,322,331,334,346,349,352,355,358,360,364,367,379,382,385,388,391,394,397,400,403,405,409,412,415,429,432,435,438,441,444,447,449,453,456,465,468,471,474,477,480,483,486,488,491,494,497,500,503,506,509,512,515,518,521,524],[11,84,85],{},"I am not against AI.",[11,87,88],{},"Let me start there — clearly.",[11,90,91],{},"Artificial intelligence is one of the most powerful tools ever created. It is accelerating medicine, research, accessibility, and creative work. In programming, it can explain complex concepts in seconds, scaffold boilerplate, suggest optimizations, and help developers break through mental blocks.",[11,93,94],{},"AI is not the enemy.",[11,96,97],{},"What concerns me is something else.",[11,99,100],{},"What concerns me is vibe coding.",[102,103],"hr",{},[18,105,107],{"id":106},"the-difference-between-using-ai-and-replacing-yourself","The Difference Between Using AI and Replacing Yourself",[11,109,110],{},"There is a meaningful distinction between leveraging AI and outsourcing your thinking to it.",[11,112,113],{},"Using AI to:",[115,116,117,121,124,127],"ul",{},[118,119,120],"li",{},"Look up an unfamiliar pattern,",[118,122,123],{},"Generate a helper function,",[118,125,126],{},"Explain a compiler error,",[118,128,129],{},"Or suggest alternative implementations —",[11,131,132],{},"That’s assistance.\nThat’s augmentation.",[11,134,135],{},"But asking it to generate entire architectures, entire features, entire products — and then shipping them without deeply understanding them — that’s something else.",[11,137,138],{},"That’s not acceleration.\nThat’s abdication.",[11,140,141],{},"And the consequences are subtle at first.",[102,143],{},[18,145,147],{"id":146},"the-illusion-of-speed","The Illusion of Speed",[11,149,150],{},"Vibe coding feels fast.",[11,152,153,154,157,158,160,161,163],{},"You prompt. ",[155,156],"br",{},"\nIt fills. ",[155,159],{},"\nYou tweak. ",[155,162],{},"\nYou ship.",[11,165,166],{},"The app runs. The UI renders. The API responds. It feels productive.",[11,168,169],{},"But fully vibe-coded programs almost never turn out as robust and secure as they need to be.",[11,171,172],{},"And that creates two very real problems.",[174,175,177],"h3",{"id":176},"_1-security-becomes-accidental","1. Security Becomes Accidental",[11,179,180],{},"Security is not something that emerges from luck.",[11,182,183],{},"It comes from careful design:",[115,185,186,189,192,195],{},[118,187,188],{},"Where is state stored?",[118,190,191],{},"How is data validated?",[118,193,194],{},"What assumptions are being made?",[118,196,197],{},"Where can this break under stress?",[11,199,200],{},"When you don’t consciously design those things, you don’t own them.",[11,202,203],{},"And if you don’t own them, you can’t defend them.",[11,205,206],{},"The more layers of generated abstraction you pile on without comprehension, the more your application becomes a black box — even to you.",[11,208,209],{},"That’s not innovation.",[11,211,212],{},"That’s roulette.",[174,214,216],{"id":215},"_2-maintenance-becomes-a-nightmare","2. Maintenance Becomes a Nightmare",[11,218,219],{},"At some point, something breaks.",[11,221,222],{},"It always does.",[11,224,225],{},"And now you’re facing a codebase you didn’t truly design:",[115,227,228,231,234],{},[118,229,230],{},"Patterns you didn’t choose.",[118,232,233],{},"Dependencies you didn’t evaluate.",[118,235,236],{},"Architectural decisions you didn’t consciously make.",[11,238,239],{},"You must reverse-engineer your own product.",[11,241,242],{},"Fixing these systems often becomes harder and more error-prone than designing them properly from scratch. Every modification feels like pulling on a thread in a sweater — you’re not sure what will unravel.",[11,244,245],{},"Speed at the beginning becomes friction at the end.",[102,247],{},[18,249,251],{"id":250},"what-were-actually-losing","What We’re Actually Losing",[11,253,254],{},"But the technical risks are not even the saddest part.",[11,256,257],{},"The saddest part is what we’re losing culturally.",[11,259,260],{},"Older software had character.",[11,262,263],{},"Not because it was objectively better.",[11,265,266],{},"But because it was designed by people who were thinking deeply.",[11,268,269],{},"Look at early applications from companies like Winamp, Nullsoft’s chaotic masterpiece. It wasn’t just a media player — it had personality. Skins. Weird visualizers. Experimental UI decisions.",[11,271,272,273,277],{},"Or early versions of Apple software under Steve Jobs — polarizing, opinionated, deliberate. You could feel that someone cared obsessively about how it ",[274,275,276],"em",{},"felt",", not just that it worked.",[11,279,280],{},"Even early indie software — small utilities written by a single developer — had quirks. Sometimes ugly. Sometimes imperfect. But distinct.",[11,282,283],{},"Now?",[11,285,286],{},"Open ten new productivity apps.",[11,288,289],{},"They:",[115,291,292,295,298,301,304],{},[118,293,294],{},"Use the same component libraries.",[118,296,297],{},"Follow the same SaaS template.",[118,299,300],{},"Have the same onboarding flow.",[118,302,303],{},"Use the same pastel gradients.",[118,305,306],{},"Use the same hero section layout.",[11,308,309],{},"Different logo. Same soul.",[11,311,312],{},"It feels like innovation has been flattened into aesthetic conformity.",[102,314],{},[18,316,318],{"id":317},"the-illusion-of-innovation","The Illusion of Innovation",[11,320,321],{},"What’s happening today is strange.",[11,323,324,325,327,328,330],{},"Someone has a genuinely good idea. ",[155,326],{},"\nSomething interesting. ",[155,329],{},"\nSomething novel.",[11,332,333],{},"But instead of carefully designing a product that expresses that idea uniquely, they take the shortest path to implementation.",[11,335,336,337,339,340,342,343,345],{},"AI scaffolds the backend. ",[155,338],{},"\nAI generates the frontend. ",[155,341],{},"\nAI writes the landing page copy. ",[155,344],{},"\nAI designs the logo.",[11,347,348],{},"And the result works.",[11,350,351],{},"But it feels like every other app launched that week.",[11,353,354],{},"We’ve optimized for speed so aggressively that we’ve sacrificed distinctiveness.",[11,356,357],{},"When everything is generated from the same statistical patterns, everything starts to feel statistically average.",[102,359],{},[18,361,363],{"id":362},"the-beauty-of-thinking","The Beauty of Thinking",[11,365,366],{},"There is something deeply beautiful about wrestling with a problem.",[11,368,369,370,372,373,375,376,378],{},"About modeling your data structures from scratch. ",[155,371],{},"\nAbout choosing an architecture intentionally. ",[155,374],{},"\nAbout rejecting an abstraction because it doesn’t feel right. ",[155,377],{},"\nAbout spending hours simplifying something that technically already works.",[11,380,381],{},"That friction is not inefficiency.",[11,383,384],{},"It is craftsmanship.",[11,386,387],{},"It is where intuition forms.",[11,389,390],{},"It is where taste develops.",[11,392,393],{},"It is where you become more than someone who “assembles code.”",[11,395,396],{},"Vibe coding short-circuits that process.",[11,398,399],{},"The generate → patch → regenerate → ship loop removes the cognitive tension that creates growth.",[11,401,402],{},"And without tension, there is no strength.",[102,404],{},[18,406,408],{"id":407},"ai-as-a-tool-not-a-substitute","AI as a Tool, Not a Substitute",[11,410,411],{},"AI should help you find what you’re looking for faster.",[11,413,414],{},"It should:",[115,416,417,420,423,426],{},[118,418,419],{},"Reduce search friction.",[118,421,422],{},"Provide reference implementations.",[118,424,425],{},"Fill in mechanical boilerplate.",[118,427,428],{},"Teach you patterns you don’t yet know.",[11,430,431],{},"Using it to generate a function you don’t know how to write — that’s fine.",[11,433,434],{},"Using it to understand a concept faster — that’s powerful.",[11,436,437],{},"But when the whole system is just… generated… and barely understood by its creator, something fundamental is missing.",[11,439,440],{},"Ownership.",[11,442,443],{},"When you write something, truly write it, you carry its logic in your head. You know why decisions were made. You know where it can break.",[11,445,446],{},"That confidence cannot be prompted into existence.",[102,448],{},[18,450,452],{"id":451},"everyone-is-in-a-hurry","Everyone Is in a Hurry",[11,454,455],{},"We are addicted to shipping.",[11,457,458,459,461,462,464],{},"Shipping fast. ",[155,460],{},"\nShipping MVP. ",[155,463],{},"\nShipping before someone else does.",[11,466,467],{},"But we rarely ask: what are we shipping into the world?",[11,469,470],{},"A temporary demo?",[11,472,473],{},"Or something designed with care?",[11,475,476],{},"Slowing down does not mean rejecting progress.",[11,478,479],{},"It means respecting complexity.",[11,481,482],{},"It means choosing depth over velocity.",[11,484,485],{},"It means valuing long-term understanding over short-term output.",[102,487],{},[18,489,80],{"id":490},"we-need-to-slow-down",[11,492,493],{},"Not because AI is bad.",[11,495,496],{},"Not because speed is evil.",[11,498,499],{},"But because quality requires thought.",[11,501,502],{},"Because security requires intention.",[11,504,505],{},"Because innovation requires courage.",[11,507,508],{},"Because character requires time.",[11,510,511],{},"The developers who truly understand their systems will always outgrow those who merely prompt them into existence.",[11,513,514],{},"We don’t need fewer tools.",[11,516,517],{},"We need more responsibility.",[11,519,520],{},"We need more taste.",[11,522,523],{},"We need more craftsmanship.",[11,525,526],{},"We need to slow down.",{"title":32,"searchDepth":33,"depth":34,"links":528},[529,530,534,535,536,537,538,539],{"id":106,"depth":33,"text":107},{"id":146,"depth":33,"text":147,"children":531},[532,533],{"id":176,"depth":34,"text":177},{"id":215,"depth":34,"text":216},{"id":250,"depth":33,"text":251},{"id":317,"depth":33,"text":318},{"id":362,"depth":33,"text":363},{"id":407,"depth":33,"text":408},{"id":451,"depth":33,"text":452},{"id":490,"depth":33,"text":80},"2026-02-27","Whatever happened to the craft?",{"author":5},"\u002Fwritings\u002Fwe-need-to-slow-down",{"title":80,"description":541},"writings\u002Fwe-need-to-slow-down","Development","8 min read","RgWkdrebCiQC3z-9tANZ07KUaJeEfEPKo05tiOOtEwM",{"id":550,"title":551,"body":552,"date":950,"description":951,"extension":38,"meta":952,"navigation":41,"path":953,"seo":954,"stem":955,"tag":956,"ttr":957,"__hash__":958},"writing\u002Fwritings\u002Ffrom-cloudflare-to-vercel.md","Why I Moved My Blog from Cloudflare to Vercel",{"type":8,"value":553,"toc":941},[554,557,560,563,571,574,576,580,583,586,589,592,599,613,616,618,622,629,632,643,646,649,666,677,680,682,686,692,695,706,709,712,737,744,806,809,818,821,823,827,830,833,844,851,854,856,860,866,869,872,875,878,880,884,887,896,899,906,908,912,915,922,931,934,937],[11,555,556],{},"For a long time, my blog lived on Cloudflare.",[11,558,559],{},"It was fast, globally distributed, inexpensive, and reliable. In many ways, it was exactly what you would want for a static or hybrid site. And for a while, it did its job well.",[11,561,562],{},"Yet recently, I moved the entire site to Vercel.",[11,564,565,566,570],{},"This post isn’t a comparison of benchmarks, edge latency, or pricing tiers. It’s a reflection on ",[567,568,569],"strong",{},"developer experience",", tooling alignment, and the cumulative weight of small frictions — especially when working with Nuxt.",[11,572,573],{},"Sometimes, infrastructure decisions are less about raw capability and more about how invisible they become over time.",[102,575],{},[18,577,579],{"id":578},"cloudflare-was-solid-just-not-invisible","Cloudflare Was Solid — Just Not Invisible",[11,581,582],{},"To be clear: Cloudflare wasn’t broken.",[11,584,585],{},"Pages, Workers, and the surrounding ecosystem are impressive. The edge-first model, the control you get over caching and routing, and the pricing structure are genuinely compelling. If you enjoy tuning infrastructure and understanding exactly how your app executes at the edge, Cloudflare is a strong choice.",[11,587,588],{},"My blog never had uptime issues. Builds worked. Performance was good.",[11,590,591],{},"But despite all that, Cloudflare never fully disappeared into the background.",[11,593,594,595,598],{},"There was always ",[274,596,597],{},"something"," to think about:",[115,600,601,604,607,610],{},[118,602,603],{},"Edge runtime differences",[118,605,606],{},"Platform-specific APIs",[118,608,609],{},"Small mismatches between local development and production",[118,611,612],{},"Nuxt features that worked, but felt slightly constrained or indirect",[11,614,615],{},"None of these were blockers. But they added friction to something that is supposed to be simple: writing and publishing.",[102,617],{},[18,619,621],{"id":620},"nuxt-feels-at-home-on-vercel","Nuxt Feels “At Home” on Vercel",[11,623,624,625,628],{},"One of the biggest factors in the move was how naturally ",[567,626,627],{},"Nuxt fits into Vercel’s mental model",".",[11,630,631],{},"Nuxt already assumes:",[115,633,634,637,640],{},[118,635,636],{},"A clear separation between server and client code",[118,638,639],{},"Predictable deployment targets",[118,641,642],{},"Opinionated defaults for SSR, SSG, and hybrid rendering",[11,644,645],{},"On Vercel, those assumptions line up almost perfectly.",[11,647,648],{},"The deployment process is boring in the best way:",[115,650,651,654,657,660,663],{},[118,652,653],{},"Push to Git",[118,655,656],{},"Automatic preview deployment",[118,658,659],{},"Production behaves exactly like preview",[118,661,662],{},"Logs are clear and accessible",[118,664,665],{},"Environment variables work as expected, without edge-specific surprises",[11,667,668,669,672,673,676],{},"I didn’t have to think about ",[274,670,671],{},"where"," my code runs. I could just think about ",[274,674,675],{},"what"," it does.",[11,678,679],{},"That alignment matters more than I expected.",[102,681],{},[18,683,685],{"id":684},"adding-vercel-analytics-to-a-nuxt-project","Adding Vercel Analytics to a Nuxt Project",[11,687,688,689,628],{},"One very tangible reason for the move was ",[567,690,691],{},"Vercel Analytics",[11,693,694],{},"I don’t want complex dashboards or third-party scripts on a personal blog. I want basic answers:",[115,696,697,700,703],{},[118,698,699],{},"Are people reading?",[118,701,702],{},"Which pages get attention?",[118,704,705],{},"Did something break after a deploy?",[11,707,708],{},"With Vercel, enabling analytics in Nuxt is almost boringly simple.",[11,710,711],{},"Install the module:",[713,714,718],"pre",{"className":715,"code":716,"language":717,"meta":32,"style":32},"language-bash shiki shiki-themes gruvbox-light-hard gruvbox-dark-hard gruvbox-dark-hard","npm install @vercel\u002Fanalytics\n","bash",[719,720,721],"code",{"__ignoreMap":32},[722,723,726,730,734],"span",{"class":724,"line":725},"line",1,[722,727,729],{"class":728},"sXcZX","npm",[722,731,733],{"class":732},"snxCs"," install",[722,735,736],{"class":732}," @vercel\u002Fanalytics\n",[11,738,739,740,743],{},"Then enable it in ",[719,741,742],{},"nuxt.config.ts",":",[713,745,749],{"className":746,"code":747,"filename":742,"language":748,"meta":32,"style":32},"language-ts shiki shiki-themes gruvbox-light-hard gruvbox-dark-hard gruvbox-dark-hard","export default defineNuxtConfig({\n    modules: [\"@vercel\u002Fanalytics\u002Fnuxt\"],\n});\n","ts",[719,750,751,771,795],{"__ignoreMap":32},[722,752,753,757,760,763,767],{"class":724,"line":725},[722,754,756],{"class":755},"sA8KM","export",[722,758,759],{"class":755}," default",[722,761,762],{"class":728}," defineNuxtConfig",[722,764,766],{"class":765},"s2xuh","(",[722,768,770],{"class":769},"swc6o","{\n",[722,772,773,776,778,781,784,787,789,792],{"class":724,"line":33},[722,774,775],{"class":765},"    modules",[722,777,743],{"class":769},[722,779,780],{"class":765}," [",[722,782,783],{"class":769},"\"",[722,785,786],{"class":732},"@vercel\u002Fanalytics\u002Fnuxt",[722,788,783],{"class":769},[722,790,791],{"class":765},"]",[722,793,794],{"class":769},",\n",[722,796,797,800,803],{"class":724,"line":34},[722,798,799],{"class":769},"}",[722,801,802],{"class":765},")",[722,804,805],{"class":769},";\n",[11,807,808],{},"That’s it.",[11,810,811,812,814,815,817],{},"No scripts to manage. ",[155,813],{},"\nNo consent popups. ",[155,816],{},"\nNo external dashboards to configure.",[11,819,820],{},"The analytics are privacy-friendly, integrated into the platform, and scoped exactly to what I need — nothing more.",[102,822],{},[18,824,826],{"id":825},"fewer-platform-specific-workarounds","Fewer Platform-Specific Workarounds",[11,828,829],{},"Another quiet benefit of the move was the reduction in platform-specific conditionals.",[11,831,832],{},"On Cloudflare, certain things required extra care:",[115,834,835,838,841],{},[118,836,837],{},"Edge runtime limitations leaking into server logic",[118,839,840],{},"Nuxt features that worked, but needed adjustments",[118,842,843],{},"APIs that behaved slightly differently depending on context",[11,845,846,847,850],{},"Again, none of this was catastrophic. But it meant thinking about the platform ",[274,848,849],{},"while"," building features.",[11,852,853],{},"On Vercel, I find myself writing more “plain Nuxt” code. The platform mostly stays out of the way, and that lowers cognitive load — especially for a solo project.",[102,855],{},[18,857,859],{"id":858},"opinionated-defaults-are-a-feature","Opinionated Defaults Are a Feature",[11,861,862,863,865],{},"Cloudflare gives you power. ",[155,864],{},"\nVercel gives you defaults.",[11,867,868],{},"At this stage, I value defaults more.",[11,870,871],{},"Routing, headers, caching behavior, previews, and framework integrations are all handled in a way that feels intentionally boring. There are fewer knobs to turn, but also fewer ways to surprise yourself.",[11,873,874],{},"For a personal site, that trade-off makes sense.",[11,876,877],{},"I don’t want infrastructure to be a creative decision.",[102,879],{},[18,881,883],{"id":882},"this-wasnt-about-speed-or-scale","This Wasn’t About Speed or Scale",[11,885,886],{},"It’s worth saying explicitly: this move wasn’t about performance.",[11,888,889,890,892,893,895],{},"Both Cloudflare and Vercel are fast. ",[155,891],{},"\nBoth scale far beyond what this site will ever need. ",[155,894],{},"\nBoth are reliable.",[11,897,898],{},"If you’re optimizing for edge control, Cloudflare might still be the better choice.",[11,900,901,902,905],{},"This move was about ",[567,903,904],{},"comfort",", predictability, and flow.",[102,907],{},[18,909,911],{"id":910},"choosing-the-platform-that-disappears","Choosing the Platform That Disappears",[11,913,914],{},"At this point, my blog isn’t an experiment. It’s a place to write, publish, and occasionally rethink ideas. The infrastructure should fade into the background.",[11,916,917,918,921],{},"Vercel does that better ",[274,919,920],{},"for me"," — especially when paired with Nuxt.",[11,923,924,925,927,928,930],{},"That doesn’t make it universally better. ",[155,926],{},"\nIt doesn’t invalidate Cloudflare. ",[155,929],{},"\nAnd it doesn’t mean I won’t move again someday.",[11,932,933],{},"It just means that right now, the trade-offs align with how I want to work.",[11,935,936],{},"And sometimes, that’s the most honest reason to move anything.",[938,939,940],"style",{},"html pre.shiki code .sXcZX, html code.shiki .sXcZX{--shiki-light:#B57614;--shiki-default:#FABD2F;--shiki-dark:#FABD2F}html pre.shiki code .snxCs, html code.shiki .snxCs{--shiki-light:#79740E;--shiki-default:#B8BB26;--shiki-dark:#B8BB26}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sA8KM, html code.shiki .sA8KM{--shiki-light:#9D0006;--shiki-default:#FB4934;--shiki-dark:#FB4934}html pre.shiki code .s2xuh, html code.shiki .s2xuh{--shiki-light:#3C3836;--shiki-default:#EBDBB2;--shiki-dark:#EBDBB2}html pre.shiki code .swc6o, html code.shiki .swc6o{--shiki-light:#7C6F64;--shiki-default:#A89984;--shiki-dark:#A89984}",{"title":32,"searchDepth":33,"depth":34,"links":942},[943,944,945,946,947,948,949],{"id":578,"depth":33,"text":579},{"id":620,"depth":33,"text":621},{"id":684,"depth":33,"text":685},{"id":825,"depth":33,"text":826},{"id":858,"depth":33,"text":859},{"id":882,"depth":33,"text":883},{"id":910,"depth":33,"text":911},"2026-02-15","not for performance, but for better flow, simpler analytics, and a smoother developer experience.",{"author":5},"\u002Fwritings\u002Ffrom-cloudflare-to-vercel",{"title":551,"description":951},"writings\u002Ffrom-cloudflare-to-vercel","Developer Experience","6 min read","LbiMKrDWpcemWcT6rWWk_uAG4cIOI-F_VSrH0czaEeo",{"id":960,"title":961,"body":962,"date":1410,"description":1411,"extension":38,"meta":1412,"navigation":41,"path":1413,"seo":1414,"stem":1415,"tag":546,"ttr":1416,"__hash__":1417},"writing\u002Fwritings\u002Fnuxt-content-slash-issue.md","How to Fix the Trailing Slash Issue in Nuxt Content",{"type":8,"value":963,"toc":1405},[964,968,974,980,983,991,1001,1008,1028,1031,1034,1038,1065,1079,1357,1360,1389,1393,1396,1399,1402],[18,965,967],{"id":966},"the-problem","The problem",[11,969,970,971,973],{},"I really enjoy using Nuxt Content. It’s simple, intuitive, and easy to manage — everything has a clear place and structure.",[155,972],{},"\nHowever, I ran into a strange issue that I can’t quite tell is a bug or just an unfortunate edge case.",[11,975,976,977,979],{},"Nuxt is responsible for handling routes, and each Nuxt Content file is automatically mapped to a route when everything is set up correctly. The problem appears when a trailing slash (",[719,978,42],{},") is added to the URL.",[11,981,982],{},"When that happens, things start to break:",[115,984,985,988],{},[118,986,987],{},"Sometimes a 404 page is shown",[118,989,990],{},"Other times the page renders, but data fetching silently fails and nothing shows up",[11,992,993,994,1000],{},"This isn’t specific to my setup. I checked ",[995,996,997],"a",{"href":997,"rel":998},"https:\u002F\u002Fwww.hrcd.fr",[999],"nofollow"," (a site I’ve taken a lot of inspiration from), and it exhibits the same behavior. Unfortunately, I couldn’t find many other real-world examples to test against, so this investigation is a bit limited.",[11,1002,1003,1004,1007],{},"What ",[274,1005,1006],{},"is"," clear is that this issue doesn’t seem to be deployment-specific. I was able to reproduce it:",[115,1009,1010,1013,1021],{},[118,1011,1012],{},"When generating the site locally",[118,1014,1015,1016],{},"When deploying to ",[995,1017,1020],{"href":1018,"rel":1019},"https:\u002F\u002Fpages.cloudflare.com",[999],"Cloudflare Pages",[118,1022,1015,1023],{},[995,1024,1027],{"href":1025,"rel":1026},"https:\u002F\u002Fvercel.com\u002Fhome",[999],"Vercel",[11,1029,1030],{},"(The hrcd.fr site is also hosted on Vercel.)",[11,1032,1033],{},"For what it’s worth, the fix described below works reliably on Cloudflare Pages. I didn’t bother re-testing it on Vercel and simply deployed my site on Cloudflare.",[18,1035,1037],{"id":1036},"the-fix","The fix",[11,1039,1040,1041,1049,1050,1052,1053,1055,1056,1059,1060,802],{},"The simplest and most reliable solution is to add a ",[567,1042,1043,1044],{},"global ",[995,1045,1048],{"href":1046,"rel":1047},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fdirectory-structure\u002Fapp\u002Fmiddleware",[999],"route middleware"," that normalizes URLs by removing trailing slashes.",[155,1051],{},"\nIf a route ends with ",[719,1054,42],{},", we permanently redirect it to the same path ",[274,1057,1058],{},"without"," the slash. (",[995,1061,1064],{"href":1062,"rel":1063},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FMiddleware",[999],"What's a middleware?",[11,1066,1067,1068,1071,1072,1075,1076,743],{},"In your Nuxt project, create a ",[719,1069,1070],{},"middleware"," directory inside the ",[719,1073,1074],{},"app"," directory (if it doesn’t already exist), then add a file called ",[719,1077,1078],{},"trailingSlash.global.ts",[713,1080,1082],{"className":746,"code":1081,"filename":1078,"language":748,"meta":32,"style":32},"export default defineNuxtRouteMiddleware((to) => {\n    \u002F\u002F ignore root and anything that's just \"\u002F\"\n    if (to.path === \"\u002F\" || to.path === \"\") return;\n\n    \u002F\u002F if it ends with \"\u002F\" and isn't just \"\u002F\"\n    if (to.path.endsWith(\"\u002F\")) {\n        const newPath = to.path.slice(0, -1) || \"\u002F\";\n        return navigateTo(\n            { path: newPath, query: to.query, hash: to.hash },\n            { redirectCode: 301 }, \u002F\u002F permanent redirect\n        );\n    }\n});\n",[719,1083,1084,1110,1116,1165,1171,1177,1208,1259,1271,1315,1334,1342,1348],{"__ignoreMap":32},[722,1085,1086,1088,1090,1093,1095,1097,1101,1103,1107],{"class":724,"line":725},[722,1087,756],{"class":755},[722,1089,759],{"class":755},[722,1091,1092],{"class":728}," defineNuxtRouteMiddleware",[722,1094,766],{"class":765},[722,1096,766],{"class":769},[722,1098,1100],{"class":1099},"sgL72","to",[722,1102,802],{"class":769},[722,1104,1106],{"class":1105},"sTCDi"," =>",[722,1108,1109],{"class":769}," {\n",[722,1111,1112],{"class":724,"line":33},[722,1113,1115],{"class":1114},"sIIdi","    \u002F\u002F ignore root and anything that's just \"\u002F\"\n",[722,1117,1118,1121,1124,1126,1128,1131,1135,1138,1140,1142,1145,1148,1150,1152,1154,1157,1160,1163],{"class":724,"line":34},[722,1119,1120],{"class":755},"    if",[722,1122,1123],{"class":765}," (",[722,1125,1100],{"class":1099},[722,1127,628],{"class":769},[722,1129,1130],{"class":1099},"path",[722,1132,1134],{"class":1133},"sHSTz"," ===",[722,1136,1137],{"class":769}," \"",[722,1139,42],{"class":732},[722,1141,783],{"class":769},[722,1143,1144],{"class":755}," ||",[722,1146,1147],{"class":1099}," to",[722,1149,628],{"class":769},[722,1151,1130],{"class":1099},[722,1153,1134],{"class":1133},[722,1155,1156],{"class":769}," \"\"",[722,1158,1159],{"class":765},") ",[722,1161,1162],{"class":755},"return",[722,1164,805],{"class":769},[722,1166,1168],{"class":724,"line":1167},4,[722,1169,1170],{"emptyLinePlaceholder":41},"\n",[722,1172,1174],{"class":724,"line":1173},5,[722,1175,1176],{"class":1114},"    \u002F\u002F if it ends with \"\u002F\" and isn't just \"\u002F\"\n",[722,1178,1180,1182,1184,1186,1188,1190,1192,1195,1197,1199,1201,1203,1206],{"class":724,"line":1179},6,[722,1181,1120],{"class":755},[722,1183,1123],{"class":765},[722,1185,1100],{"class":1099},[722,1187,628],{"class":769},[722,1189,1130],{"class":1099},[722,1191,628],{"class":769},[722,1193,1194],{"class":728},"endsWith",[722,1196,766],{"class":765},[722,1198,783],{"class":769},[722,1200,42],{"class":732},[722,1202,783],{"class":769},[722,1204,1205],{"class":765},")) ",[722,1207,770],{"class":769},[722,1209,1211,1214,1217,1220,1222,1224,1226,1228,1231,1233,1237,1240,1243,1246,1248,1251,1253,1255,1257],{"class":724,"line":1210},7,[722,1212,1213],{"class":1105},"        const",[722,1215,1216],{"class":1099}," newPath",[722,1218,1219],{"class":1133}," =",[722,1221,1147],{"class":1099},[722,1223,628],{"class":769},[722,1225,1130],{"class":1099},[722,1227,628],{"class":769},[722,1229,1230],{"class":728},"slice",[722,1232,766],{"class":765},[722,1234,1236],{"class":1235},"sYNTs","0",[722,1238,1239],{"class":769},",",[722,1241,1242],{"class":1133}," -",[722,1244,1245],{"class":1235},"1",[722,1247,1159],{"class":765},[722,1249,1250],{"class":755},"||",[722,1252,1137],{"class":769},[722,1254,42],{"class":732},[722,1256,783],{"class":769},[722,1258,805],{"class":769},[722,1260,1262,1265,1268],{"class":724,"line":1261},8,[722,1263,1264],{"class":755},"        return",[722,1266,1267],{"class":728}," navigateTo",[722,1269,1270],{"class":765},"(\n",[722,1272,1274,1277,1280,1282,1284,1286,1289,1291,1293,1295,1298,1300,1303,1305,1307,1309,1312],{"class":724,"line":1273},9,[722,1275,1276],{"class":769},"            {",[722,1278,1279],{"class":765}," path",[722,1281,743],{"class":769},[722,1283,1216],{"class":1099},[722,1285,1239],{"class":769},[722,1287,1288],{"class":765}," query",[722,1290,743],{"class":769},[722,1292,1147],{"class":1099},[722,1294,628],{"class":769},[722,1296,1297],{"class":1099},"query",[722,1299,1239],{"class":769},[722,1301,1302],{"class":765}," hash",[722,1304,743],{"class":769},[722,1306,1147],{"class":1099},[722,1308,628],{"class":769},[722,1310,1311],{"class":1099},"hash",[722,1313,1314],{"class":769}," },\n",[722,1316,1318,1320,1323,1325,1328,1331],{"class":724,"line":1317},10,[722,1319,1276],{"class":769},[722,1321,1322],{"class":765}," redirectCode",[722,1324,743],{"class":769},[722,1326,1327],{"class":1235}," 301",[722,1329,1330],{"class":769}," },",[722,1332,1333],{"class":1114}," \u002F\u002F permanent redirect\n",[722,1335,1337,1340],{"class":724,"line":1336},11,[722,1338,1339],{"class":765},"        )",[722,1341,805],{"class":769},[722,1343,1345],{"class":724,"line":1344},12,[722,1346,1347],{"class":769},"    }\n",[722,1349,1351,1353,1355],{"class":724,"line":1350},13,[722,1352,799],{"class":769},[722,1354,802],{"class":765},[722,1356,805],{"class":769},[11,1358,1359],{},"This middleware:",[115,1361,1362,1368,1375,1378],{},[118,1363,1364,1365,1367],{},"Leaves the root route (",[719,1366,42],{},") untouched",[118,1369,1370,1371,1374],{},"Only redirects paths that ",[274,1372,1373],{},"end"," with a trailing slash",[118,1376,1377],{},"Preserves query parameters and hashes",[118,1379,1380,1381,1388],{},"Uses a ",[567,1382,1383],{},[995,1384,1387],{"href":1385,"rel":1386},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FReference\u002FStatus\u002F301",[999],"301 (Permanent Redirect)",", which tells browsers and search engines that the canonical URL no longer includes the trailing slash",[18,1390,1392],{"id":1391},"final-words","Final words",[11,1394,1395],{},"This isn’t the most elegant solution, and ideally Nuxt Content (or Nuxt’s routing layer) would handle this more gracefully out of the box. Still, until that happens, a small global middleware like this is an effective and low-maintenance workaround.",[11,1397,1398],{},"If your Nuxt Content pages randomly break, fail to fetch data, or show 404s depending on whether a trailing slash is present, normalizing routes early is a good defensive move — especially for statically generated sites where consistency matters.",[11,1400,1401],{},"Hopefully this saves someone else a few hours of confusion.",[938,1403,1404],{},"html pre.shiki code .sA8KM, html code.shiki .sA8KM{--shiki-light:#9D0006;--shiki-default:#FB4934;--shiki-dark:#FB4934}html pre.shiki code .sXcZX, html code.shiki .sXcZX{--shiki-light:#B57614;--shiki-default:#FABD2F;--shiki-dark:#FABD2F}html pre.shiki code .s2xuh, html code.shiki .s2xuh{--shiki-light:#3C3836;--shiki-default:#EBDBB2;--shiki-dark:#EBDBB2}html pre.shiki code .swc6o, html code.shiki .swc6o{--shiki-light:#7C6F64;--shiki-default:#A89984;--shiki-dark:#A89984}html pre.shiki code .sgL72, html code.shiki .sgL72{--shiki-light:#076678;--shiki-default:#83A598;--shiki-dark:#83A598}html pre.shiki code .sTCDi, html code.shiki .sTCDi{--shiki-light:#AF3A03;--shiki-default:#FE8019;--shiki-dark:#FE8019}html pre.shiki code .sIIdi, html code.shiki .sIIdi{--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-default:#928374;--shiki-default-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic}html pre.shiki code .sHSTz, html code.shiki .sHSTz{--shiki-light:#427B58;--shiki-default:#8EC07C;--shiki-dark:#8EC07C}html pre.shiki code .snxCs, html code.shiki .snxCs{--shiki-light:#79740E;--shiki-default:#B8BB26;--shiki-dark:#B8BB26}html pre.shiki code .sYNTs, html code.shiki .sYNTs{--shiki-light:#8F3F71;--shiki-default:#D3869B;--shiki-dark:#D3869B}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":32,"searchDepth":33,"depth":34,"links":1406},[1407,1408,1409],{"id":966,"depth":33,"text":967},{"id":1036,"depth":33,"text":1037},{"id":1391,"depth":33,"text":1392},"2025-12-31","A weird routing issue with Nuxt Content URLs — and a practical fix",{"author":5},"\u002Fwritings\u002Fnuxt-content-slash-issue",{"title":961,"description":1411},"writings\u002Fnuxt-content-slash-issue","3 min read","raUKr3sH0PndZmgimUPRDBbAOSXjRJ_qEijbbl3WUpk",1789387126132]