[{"data":1,"prerenderedAt":3073},["ShallowReactive",2],{"post-\u002Fposts\u002Fbuild-a-url-shortener-with-nitro-on-cloudflare-pages":3,"post-link-en-external":3068},{"id":4,"title":5,"body":6,"contentId":3061,"date":3062,"description":3063,"extension":3064,"meta":3065,"navigation":369,"path":3066,"rawbody":3067,"robots":3068,"schemaOrg":3068,"seo":3069,"sitemap":3070,"stem":3071,"__hash__":3072},"posts\u002Fposts\u002Fbuild-a-url-shortener-with-nitro-on-cloudflare-pages.md","Build a URL Shortener with Nitro on Cloudflare Pages",{"type":7,"value":8,"toc":3054},"minimark",[9,36,43,52,65,79,82,143,148,155,184,190,212,219,234,242,246,249,267,272,291,804,814,825,832,1065,1074,1078,1085,1099,1118,1684,1698,1711,1721,1735,3050],[10,11,12,13,17,18,27,28,35],"p",{},"In this article, we will develop a ",[14,15,16],"strong",{},"URL shortener"," utilizing ",[19,20,24],"a",{"href":21,"rel":22},"https:\u002F\u002Fnitro.unjs.io",[23],"nofollow",[14,25,26],{},"Nitro"," and deploy it on ",[19,29,32],{"href":30,"rel":31},"https:\u002F\u002Fpages.cloudflare.com\u002F",[23],[14,33,34],{},"Cloudflare Pages",".",[10,37,38,39],{},"The source code is accessible on ",[40,41,35],"git-hub-link",{"repo":42},"barbapapazes\u002Furl-shortener",[10,44,45,47,48,51],{},[14,46,26],{}," represents a new generation of ",[14,49,50],{},"server toolkit",". It empowers us to construct web servers with all necessary functionalities and deploy them at our convenience.",[10,53,54,56,57,64],{},[14,55,34],{}," is a platform designed to build and host websites on the edge. It supports services like ",[19,58,61],{"href":59,"rel":60},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fkv\u002F",[23],[14,62,63],{},"KV"," to create comprehensive full-stack stateful applications.",[10,66,67,68,70,71,74,75,78],{},"Our project is a straightforward ",[14,69,16],{}," that facilitates converting a long URL into a shortened version. We will leverage the ",[14,72,73],{},"Cloudflare KV"," to store the URLs and the ",[14,76,77],{},"Nitro server"," to manage requests.",[10,80,81],{},"We will utilize:",[83,84,85,102,116,133],"ul",{},[86,87,88,95,96,98,99,35],"li",{},[19,89,92],{"href":90,"rel":91},"https:\u002F\u002Funstorage.unjs.io",[23],[14,93,94],{},"unstorage"," to streamline the development process by abstracting the ",[14,97,63],{}," layer, eliminating the need for the ",[14,100,101],{},"Cloudflare Wrangler CLI",[86,103,104,111,112,115],{},[19,105,108],{"href":106,"rel":107},"https:\u002F\u002Fohash.unjs.io",[23],[14,109,110],{},"ohash"," to generate a ",[14,113,114],{},"hash"," from the URL to prevent collisions.",[86,117,118,125,126,129,130,35],{},[19,119,122],{"href":120,"rel":121},"https:\u002F\u002Fnanojsx.io\u002F",[23],[14,123,124],{},"nanojsx"," to build the ",[14,127,128],{},"HTML pages"," using ",[14,131,132],{},"TSX",[86,134,135,142],{},[19,136,139],{"href":137,"rel":138},"https:\u002F\u002Fpicocss.com",[23],[14,140,141],{},"pico.css"," for styling the application.",[144,145,147],"h2",{"id":146},"project-initialization","Project initialization",[10,149,150,151,154],{},"First, create a new ",[14,152,153],{},"Nitro project",":",[156,157,162],"pre",{"className":158,"code":159,"language":160,"meta":161,"style":161},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","npx giget@latest nitro url-shortener\n","bash","",[163,164,165],"code",{"__ignoreMap":161},[166,167,170,174,178,181],"span",{"class":168,"line":169},"line",1,[166,171,173],{"class":172},"sBMFI","npx",[166,175,177],{"class":176},"sfazB"," giget@latest",[166,179,180],{"class":176}," nitro",[166,182,183],{"class":176}," url-shortener\n",[10,185,186,187,154],{},"Subsequently, navigate to the project and install the required ",[14,188,189],{},"dependencies",[156,191,193],{"className":158,"code":192,"language":160,"meta":161,"style":161},"cd url-shortener\nnpm install\n",[163,194,195,203],{"__ignoreMap":161},[166,196,197,201],{"class":168,"line":169},[166,198,200],{"class":199},"s2Zo4","cd",[166,202,183],{"class":176},[166,204,206,209],{"class":168,"line":205},2,[166,207,208],{"class":172},"npm",[166,210,211],{"class":176}," install\n",[10,213,214,215,218],{},"Start the ",[14,216,217],{},"development server"," to view the default Nitro page:",[156,220,222],{"className":158,"code":221,"language":160,"meta":161,"style":161},"npm run dev\n",[163,223,224],{"__ignoreMap":161},[166,225,226,228,231],{"class":168,"line":169},[166,227,208],{"class":172},[166,229,230],{"class":176}," run",[166,232,233],{"class":176}," dev\n",[10,235,236,237,241],{},"Open your browser and visit ",[19,238,239],{"href":239,"rel":240},"http:\u002F\u002Flocalhost:3000",[23]," to verify functionality.",[144,243,245],{"id":244},"constructing-the-url-shortener","Constructing the URL shortener",[10,247,248],{},"Initially, install the necessary packages:",[156,250,252],{"className":158,"code":251,"language":160,"meta":161,"style":161},"npm install ohash nano-jsx\n",[163,253,254],{"__ignoreMap":161},[166,255,256,258,261,264],{"class":168,"line":169},[166,257,208],{"class":172},[166,259,260],{"class":176}," install",[166,262,263],{"class":176}," ohash",[166,265,266],{"class":176}," nano-jsx\n",[268,269,271],"h3",{"id":270},"generate-a-short-url","Generate a short URL",[10,273,274,275,278,279,282,283,286,287,290],{},"Create a ",[14,276,277],{},"route"," named ",[163,280,281],{},"index.get.tsx"," within the ",[163,284,285],{},"server\u002Froutes"," directory. This will serve as the ",[14,288,289],{},"home page"," of our URL shortener where users can generate a shortened URL from a long one.",[156,292,297],{"className":293,"code":294,"filename":295,"language":296,"meta":161,"style":161},"language-tsx shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import { h, Helmet, renderSSR } from 'nano-jsx' \u002F\u002F the `h` is critical here\nimport { withTemplate } from '..\u002Fresources\u002Ftemplate'\n\nexport default defineLazyEventHandler(() => {\n  const App = () => {\n    return (\n      \u003Cdiv>\n        \u003CHelmet>\n          \u003Ctitle>URL Shortener with Nitro\u003C\u002Ftitle>\n        \u003C\u002FHelmet>\n        \u003Ch2>Shorten a URL\u003C\u002Fh2>\n        \u003Cform action=\"\u002Fcreate\" method=\"POST\">\n          \u003Cinput type=\"url\" name=\"url\" placeholder=\"URL to shorten\" autocomplete=\"off\" \u002F>\n          \u003Cbutton type=\"submit\">Create\u003C\u002Fbutton>\n        \u003C\u002Fform>\n      \u003C\u002Fdiv>\n    )\n  }\n  const app = renderSSR(\u003CApp \u002F>)\n  const { body, head } = Helmet.SSR(app)\n\n  const page = withTemplate({\n    body,\n    head,\n  })\n\n  return defineEventHandler(() => {\n    return page\n  })\n})\n","server\u002Froutes\u002Findex.get.tsx","tsx",[163,298,299,343,364,371,396,415,425,437,448,470,480,498,534,592,622,631,641,647,653,679,712,717,734,743,751,759,764,781,789,796],{"__ignoreMap":161},[166,300,301,305,309,313,316,319,321,324,327,330,333,336,339],{"class":168,"line":169},[166,302,304],{"class":303},"s7zQu","import",[166,306,308],{"class":307},"sMK4o"," {",[166,310,312],{"class":311},"sTEyZ"," h",[166,314,315],{"class":307},",",[166,317,318],{"class":311}," Helmet",[166,320,315],{"class":307},[166,322,323],{"class":311}," renderSSR",[166,325,326],{"class":307}," }",[166,328,329],{"class":303}," from",[166,331,332],{"class":307}," '",[166,334,335],{"class":176},"nano-jsx",[166,337,338],{"class":307},"'",[166,340,342],{"class":341},"sHwdD"," \u002F\u002F the `h` is critical here\n",[166,344,345,347,349,352,354,356,358,361],{"class":168,"line":205},[166,346,304],{"class":303},[166,348,308],{"class":307},[166,350,351],{"class":311}," withTemplate",[166,353,326],{"class":307},[166,355,329],{"class":303},[166,357,332],{"class":307},[166,359,360],{"class":176},"..\u002Fresources\u002Ftemplate",[166,362,363],{"class":307},"'\n",[166,365,367],{"class":168,"line":366},3,[166,368,370],{"emptyLinePlaceholder":369},true,"\n",[166,372,374,377,380,383,386,389,393],{"class":168,"line":373},4,[166,375,376],{"class":303},"export",[166,378,379],{"class":303}," default",[166,381,382],{"class":199}," defineLazyEventHandler",[166,384,385],{"class":311},"(",[166,387,388],{"class":307},"()",[166,390,392],{"class":391},"spNyl"," =>",[166,394,395],{"class":307}," {\n",[166,397,399,402,405,408,411,413],{"class":168,"line":398},5,[166,400,401],{"class":391},"  const",[166,403,404],{"class":311}," App",[166,406,407],{"class":307}," =",[166,409,410],{"class":307}," ()",[166,412,392],{"class":391},[166,414,395],{"class":307},[166,416,418,421],{"class":168,"line":417},6,[166,419,420],{"class":303},"    return",[166,422,424],{"class":423},"swJcz"," (\n",[166,426,428,431,434],{"class":168,"line":427},7,[166,429,430],{"class":307},"      \u003C",[166,432,433],{"class":423},"div",[166,435,436],{"class":307},">\n",[166,438,440,443,446],{"class":168,"line":439},8,[166,441,442],{"class":307},"        \u003C",[166,444,445],{"class":172},"Helmet",[166,447,436],{"class":307},[166,449,451,454,457,460,463,466,468],{"class":168,"line":450},9,[166,452,453],{"class":307},"          \u003C",[166,455,456],{"class":423},"title",[166,458,459],{"class":307},">",[166,461,462],{"class":311},"URL Shortener with Nitro",[166,464,465],{"class":307},"\u003C\u002F",[166,467,456],{"class":423},[166,469,436],{"class":307},[166,471,473,476,478],{"class":168,"line":472},10,[166,474,475],{"class":307},"        \u003C\u002F",[166,477,445],{"class":172},[166,479,436],{"class":307},[166,481,483,485,487,489,492,494,496],{"class":168,"line":482},11,[166,484,442],{"class":307},[166,486,144],{"class":423},[166,488,459],{"class":307},[166,490,491],{"class":311},"Shorten a URL",[166,493,465],{"class":307},[166,495,144],{"class":423},[166,497,436],{"class":307},[166,499,501,503,506,509,512,515,518,520,523,525,527,530,532],{"class":168,"line":500},12,[166,502,442],{"class":307},[166,504,505],{"class":423},"form",[166,507,508],{"class":391}," action",[166,510,511],{"class":307},"=",[166,513,514],{"class":307},"\"",[166,516,517],{"class":176},"\u002Fcreate",[166,519,514],{"class":307},[166,521,522],{"class":391}," method",[166,524,511],{"class":307},[166,526,514],{"class":307},[166,528,529],{"class":176},"POST",[166,531,514],{"class":307},[166,533,436],{"class":307},[166,535,537,539,542,545,547,549,552,554,557,559,561,563,565,568,570,572,575,577,580,582,584,587,589],{"class":168,"line":536},13,[166,538,453],{"class":307},[166,540,541],{"class":423},"input",[166,543,544],{"class":391}," type",[166,546,511],{"class":307},[166,548,514],{"class":307},[166,550,551],{"class":176},"url",[166,553,514],{"class":307},[166,555,556],{"class":391}," name",[166,558,511],{"class":307},[166,560,514],{"class":307},[166,562,551],{"class":176},[166,564,514],{"class":307},[166,566,567],{"class":391}," placeholder",[166,569,511],{"class":307},[166,571,514],{"class":307},[166,573,574],{"class":176},"URL to shorten",[166,576,514],{"class":307},[166,578,579],{"class":391}," autocomplete",[166,581,511],{"class":307},[166,583,514],{"class":307},[166,585,586],{"class":176},"off",[166,588,514],{"class":307},[166,590,591],{"class":307}," \u002F>\n",[166,593,595,597,600,602,604,606,609,611,613,616,618,620],{"class":168,"line":594},14,[166,596,453],{"class":307},[166,598,599],{"class":423},"button",[166,601,544],{"class":391},[166,603,511],{"class":307},[166,605,514],{"class":307},[166,607,608],{"class":176},"submit",[166,610,514],{"class":307},[166,612,459],{"class":307},[166,614,615],{"class":311},"Create",[166,617,465],{"class":307},[166,619,599],{"class":423},[166,621,436],{"class":307},[166,623,625,627,629],{"class":168,"line":624},15,[166,626,475],{"class":307},[166,628,505],{"class":423},[166,630,436],{"class":307},[166,632,634,637,639],{"class":168,"line":633},16,[166,635,636],{"class":307},"      \u003C\u002F",[166,638,433],{"class":423},[166,640,436],{"class":307},[166,642,644],{"class":168,"line":643},17,[166,645,646],{"class":423},"    )\n",[166,648,650],{"class":168,"line":649},18,[166,651,652],{"class":307},"  }\n",[166,654,656,658,661,663,665,667,670,673,676],{"class":168,"line":655},19,[166,657,401],{"class":391},[166,659,660],{"class":311}," app",[166,662,407],{"class":307},[166,664,323],{"class":199},[166,666,385],{"class":423},[166,668,669],{"class":307},"\u003C",[166,671,672],{"class":172},"App",[166,674,675],{"class":307}," \u002F>",[166,677,678],{"class":423},")\n",[166,680,682,684,686,689,691,694,696,698,700,702,705,707,710],{"class":168,"line":681},20,[166,683,401],{"class":391},[166,685,308],{"class":307},[166,687,688],{"class":311}," body",[166,690,315],{"class":307},[166,692,693],{"class":311}," head",[166,695,326],{"class":307},[166,697,407],{"class":307},[166,699,318],{"class":311},[166,701,35],{"class":307},[166,703,704],{"class":199},"SSR",[166,706,385],{"class":423},[166,708,709],{"class":311},"app",[166,711,678],{"class":423},[166,713,715],{"class":168,"line":714},21,[166,716,370],{"emptyLinePlaceholder":369},[166,718,720,722,725,727,729,731],{"class":168,"line":719},22,[166,721,401],{"class":391},[166,723,724],{"class":311}," page",[166,726,407],{"class":307},[166,728,351],{"class":199},[166,730,385],{"class":423},[166,732,733],{"class":307},"{\n",[166,735,737,740],{"class":168,"line":736},23,[166,738,739],{"class":311},"    body",[166,741,742],{"class":307},",\n",[166,744,746,749],{"class":168,"line":745},24,[166,747,748],{"class":311},"    head",[166,750,742],{"class":307},[166,752,754,757],{"class":168,"line":753},25,[166,755,756],{"class":307},"  }",[166,758,678],{"class":423},[166,760,762],{"class":168,"line":761},26,[166,763,370],{"emptyLinePlaceholder":369},[166,765,767,770,773,775,777,779],{"class":168,"line":766},27,[166,768,769],{"class":303},"  return",[166,771,772],{"class":199}," defineEventHandler",[166,774,385],{"class":423},[166,776,388],{"class":307},[166,778,392],{"class":391},[166,780,395],{"class":307},[166,782,784,786],{"class":168,"line":783},28,[166,785,420],{"class":303},[166,787,788],{"class":311}," page\n",[166,790,792,794],{"class":168,"line":791},29,[166,793,756],{"class":307},[166,795,678],{"class":423},[166,797,799,802],{"class":168,"line":798},30,[166,800,801],{"class":307},"}",[166,803,678],{"class":311},[10,805,806,807,810,811,813],{},"This route will present a form enabling users to input a URL to shorten. Upon form submission, a ",[14,808,809],{},"POST request"," is sent to the ",[163,812,517],{}," route.",[10,815,816,817,820,821,824],{},"A ",[14,818,819],{},"lazy event handler"," is utilized to generate the view only once, when a request reaches the server. The response is then ",[14,822,823],{},"cached in-memory"," and reused for future requests, reducing the computational load.",[10,826,827,828,831],{},"The ",[163,829,830],{},"withTemplate"," function serves as a utility that we must construct.",[156,833,838],{"className":834,"code":835,"filename":836,"language":837,"meta":161,"style":161},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","interface LayoutProps {\n  body: string\n  head: string[]\n}\n\nexport function withTemplate(props: LayoutProps) {\n  const { head, body } = props\n\n  return \u002F* html *\u002F`\u003Chtml>\n      \u003Chead>\n       \u003Clink\n          rel=\"stylesheet\"\n          href=\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002F@picocss\u002Fpico@2\u002Fcss\u002Fpico.min.css\"\n        \u002F>\n        ${head.join('\\n')}\n      \u003C\u002Fhead>\n      \u003Cbody>\n        \u003Cheader class=\"container\">\n          \u003Ch1>\n            \u003Ca href=\"\u002F\">URL Shortener with Nitro\u003C\u002Fa>\n          \u003C\u002Fh1>\n        \u003C\u002Fheader>\n        \u003Cmain class=\"container\">\n          ${body}\n        \u003C\u002Fmain>\n      \u003C\u002Fbody>\n    \u003C\u002Fhtml>`\n}\n","server\u002Fresources\u002Ftemplate.tsx","ts",[163,839,840,850,860,873,878,882,906,925,929,942,947,952,957,962,967,993,998,1003,1008,1013,1018,1023,1028,1033,1043,1048,1053,1061],{"__ignoreMap":161},[166,841,842,845,848],{"class":168,"line":169},[166,843,844],{"class":391},"interface",[166,846,847],{"class":172}," LayoutProps",[166,849,395],{"class":307},[166,851,852,855,857],{"class":168,"line":205},[166,853,854],{"class":423},"  body",[166,856,154],{"class":307},[166,858,859],{"class":172}," string\n",[166,861,862,865,867,870],{"class":168,"line":366},[166,863,864],{"class":423},"  head",[166,866,154],{"class":307},[166,868,869],{"class":172}," string",[166,871,872],{"class":311},"[]\n",[166,874,875],{"class":168,"line":373},[166,876,877],{"class":307},"}\n",[166,879,880],{"class":168,"line":398},[166,881,370],{"emptyLinePlaceholder":369},[166,883,884,886,889,891,893,897,899,901,904],{"class":168,"line":417},[166,885,376],{"class":303},[166,887,888],{"class":391}," function",[166,890,351],{"class":199},[166,892,385],{"class":307},[166,894,896],{"class":895},"sHdIc","props",[166,898,154],{"class":307},[166,900,847],{"class":172},[166,902,903],{"class":307},")",[166,905,395],{"class":307},[166,907,908,910,912,914,916,918,920,922],{"class":168,"line":427},[166,909,401],{"class":391},[166,911,308],{"class":307},[166,913,693],{"class":311},[166,915,315],{"class":307},[166,917,688],{"class":311},[166,919,326],{"class":307},[166,921,407],{"class":307},[166,923,924],{"class":311}," props\n",[166,926,927],{"class":168,"line":439},[166,928,370],{"emptyLinePlaceholder":369},[166,930,931,933,936,939],{"class":168,"line":450},[166,932,769],{"class":303},[166,934,935],{"class":341}," \u002F* html *\u002F",[166,937,938],{"class":307},"`",[166,940,941],{"class":176},"\u003Chtml>\n",[166,943,944],{"class":168,"line":472},[166,945,946],{"class":176},"      \u003Chead>\n",[166,948,949],{"class":168,"line":482},[166,950,951],{"class":176},"       \u003Clink\n",[166,953,954],{"class":168,"line":500},[166,955,956],{"class":176},"          rel=\"stylesheet\"\n",[166,958,959],{"class":168,"line":536},[166,960,961],{"class":176},"          href=\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002F@picocss\u002Fpico@2\u002Fcss\u002Fpico.min.css\"\n",[166,963,964],{"class":168,"line":594},[166,965,966],{"class":176},"        \u002F>\n",[166,968,969,972,975,977,980,982,984,987,989,991],{"class":168,"line":624},[166,970,971],{"class":307},"        ${",[166,973,974],{"class":311},"head",[166,976,35],{"class":307},[166,978,979],{"class":199},"join",[166,981,385],{"class":311},[166,983,338],{"class":307},[166,985,986],{"class":311},"\\n",[166,988,338],{"class":307},[166,990,903],{"class":311},[166,992,877],{"class":307},[166,994,995],{"class":168,"line":633},[166,996,997],{"class":176},"      \u003C\u002Fhead>\n",[166,999,1000],{"class":168,"line":643},[166,1001,1002],{"class":176},"      \u003Cbody>\n",[166,1004,1005],{"class":168,"line":649},[166,1006,1007],{"class":176},"        \u003Cheader class=\"container\">\n",[166,1009,1010],{"class":168,"line":655},[166,1011,1012],{"class":176},"          \u003Ch1>\n",[166,1014,1015],{"class":168,"line":681},[166,1016,1017],{"class":176},"            \u003Ca href=\"\u002F\">URL Shortener with Nitro\u003C\u002Fa>\n",[166,1019,1020],{"class":168,"line":714},[166,1021,1022],{"class":176},"          \u003C\u002Fh1>\n",[166,1024,1025],{"class":168,"line":719},[166,1026,1027],{"class":176},"        \u003C\u002Fheader>\n",[166,1029,1030],{"class":168,"line":736},[166,1031,1032],{"class":176},"        \u003Cmain class=\"container\">\n",[166,1034,1035,1038,1041],{"class":168,"line":745},[166,1036,1037],{"class":307},"          ${",[166,1039,1040],{"class":311},"body",[166,1042,877],{"class":307},[166,1044,1045],{"class":168,"line":753},[166,1046,1047],{"class":176},"        \u003C\u002Fmain>\n",[166,1049,1050],{"class":168,"line":761},[166,1051,1052],{"class":176},"      \u003C\u002Fbody>\n",[166,1054,1055,1058],{"class":168,"line":766},[166,1056,1057],{"class":176},"    \u003C\u002Fhtml>",[166,1059,1060],{"class":307},"`\n",[166,1062,1063],{"class":168,"line":783},[166,1064,877],{"class":307},[10,1066,1067,1068,1071,1072,35],{},"This is a simple ",[14,1069,1070],{},"string template"," where we incorporate the head and body content generated by ",[14,1073,335],{},[268,1075,1077],{"id":1076},"url-storage","URL storage",[10,1079,1080,1081,1084],{},"Before proceeding, install ",[163,1082,1083],{},"zod"," to validate the request body.",[156,1086,1088],{"className":158,"code":1087,"language":160,"meta":161,"style":161},"npm install zod\n",[163,1089,1090],{"__ignoreMap":161},[166,1091,1092,1094,1096],{"class":168,"line":169},[166,1093,208],{"class":172},[166,1095,260],{"class":176},[166,1097,1098],{"class":176}," zod\n",[10,1100,1101,1102,1104,1105,1107,1108,1110,1111,1114,1115,1117],{},"Create the ",[163,1103,517],{}," route to manage the ",[14,1106,809],{}," and store the URL in the ",[14,1109,63],{},". This route is labeled ",[163,1112,1113],{},"create.post.tsx"," and resides in the ",[163,1116,285],{}," directory.",[156,1119,1122],{"className":293,"code":1120,"filename":1121,"language":296,"meta":161,"style":161},"import { h, Helmet, renderSSR } from 'nano-jsx'\nimport { hash } from 'ohash'\nimport { z } from 'zod' \u002F\u002F the `h` is essential here\nimport { withTemplate } from '..\u002Fresources\u002Ftemplate'\n\nexport default defineEventHandler(async (event) => {\n  const body = await readValidatedBody(event, z.object({\n    url: z.string().url(),\n  }).parse)\n\n  const requestURL = getRequestURL(event)\n  const id = hash(body.url)\n  const shortenURL = new URL(`\u002F${id}`, requestURL).href\n\n  await useStorage('data').setItem(id, body.url)\n\n  const App = () => {\n    return (\n      \u003Cdiv>\n        \u003CHelmet>\n          \u003Ctitle>Created\u003C\u002Ftitle>\n        \u003C\u002FHelmet>\n        \u003Ch2>Created and Ready\u003C\u002Fh2>\n        \u003Cinput\n          type=\"text\"\n          value={shortenURL}\n          autofocus\n        \u002F>\n      \u003C\u002Fdiv>\n    )\n  }\n\n  const app = renderSSR(\u003CApp \u002F>)\n  const { body: nanoBody, head } = Helmet.SSR(app)\n\n  return withTemplate({\n    body: nanoBody,\n    head,\n  })\n})\n","server\u002Froutes\u002Fcreate.post.tsx",[163,1123,1124,1150,1169,1191,1209,1213,1238,1269,1293,1306,1310,1328,1349,1391,1395,1433,1437,1451,1457,1465,1473,1490,1498,1515,1522,1537,1550,1555,1559,1567,1571,1576,1581,1602,1636,1641,1652,1663,1670,1677],{"__ignoreMap":161},[166,1125,1126,1128,1130,1132,1134,1136,1138,1140,1142,1144,1146,1148],{"class":168,"line":169},[166,1127,304],{"class":303},[166,1129,308],{"class":307},[166,1131,312],{"class":311},[166,1133,315],{"class":307},[166,1135,318],{"class":311},[166,1137,315],{"class":307},[166,1139,323],{"class":311},[166,1141,326],{"class":307},[166,1143,329],{"class":303},[166,1145,332],{"class":307},[166,1147,335],{"class":176},[166,1149,363],{"class":307},[166,1151,1152,1154,1156,1159,1161,1163,1165,1167],{"class":168,"line":205},[166,1153,304],{"class":303},[166,1155,308],{"class":307},[166,1157,1158],{"class":311}," hash",[166,1160,326],{"class":307},[166,1162,329],{"class":303},[166,1164,332],{"class":307},[166,1166,110],{"class":176},[166,1168,363],{"class":307},[166,1170,1171,1173,1175,1178,1180,1182,1184,1186,1188],{"class":168,"line":366},[166,1172,304],{"class":303},[166,1174,308],{"class":307},[166,1176,1177],{"class":311}," z",[166,1179,326],{"class":307},[166,1181,329],{"class":303},[166,1183,332],{"class":307},[166,1185,1083],{"class":176},[166,1187,338],{"class":307},[166,1189,1190],{"class":341}," \u002F\u002F the `h` is essential here\n",[166,1192,1193,1195,1197,1199,1201,1203,1205,1207],{"class":168,"line":373},[166,1194,304],{"class":303},[166,1196,308],{"class":307},[166,1198,351],{"class":311},[166,1200,326],{"class":307},[166,1202,329],{"class":303},[166,1204,332],{"class":307},[166,1206,360],{"class":176},[166,1208,363],{"class":307},[166,1210,1211],{"class":168,"line":398},[166,1212,370],{"emptyLinePlaceholder":369},[166,1214,1215,1217,1219,1221,1223,1226,1229,1232,1234,1236],{"class":168,"line":417},[166,1216,376],{"class":303},[166,1218,379],{"class":303},[166,1220,772],{"class":199},[166,1222,385],{"class":311},[166,1224,1225],{"class":391},"async",[166,1227,1228],{"class":307}," (",[166,1230,1231],{"class":895},"event",[166,1233,903],{"class":307},[166,1235,392],{"class":391},[166,1237,395],{"class":307},[166,1239,1240,1242,1244,1246,1249,1252,1254,1256,1258,1260,1262,1265,1267],{"class":168,"line":427},[166,1241,401],{"class":391},[166,1243,688],{"class":311},[166,1245,407],{"class":307},[166,1247,1248],{"class":303}," await",[166,1250,1251],{"class":199}," readValidatedBody",[166,1253,385],{"class":423},[166,1255,1231],{"class":311},[166,1257,315],{"class":307},[166,1259,1177],{"class":311},[166,1261,35],{"class":307},[166,1263,1264],{"class":199},"object",[166,1266,385],{"class":423},[166,1268,733],{"class":307},[166,1270,1271,1274,1276,1278,1280,1283,1285,1287,1289,1291],{"class":168,"line":439},[166,1272,1273],{"class":423},"    url",[166,1275,154],{"class":307},[166,1277,1177],{"class":311},[166,1279,35],{"class":307},[166,1281,1282],{"class":199},"string",[166,1284,388],{"class":423},[166,1286,35],{"class":307},[166,1288,551],{"class":199},[166,1290,388],{"class":423},[166,1292,742],{"class":307},[166,1294,1295,1297,1299,1301,1304],{"class":168,"line":450},[166,1296,756],{"class":307},[166,1298,903],{"class":423},[166,1300,35],{"class":307},[166,1302,1303],{"class":311},"parse",[166,1305,678],{"class":423},[166,1307,1308],{"class":168,"line":472},[166,1309,370],{"emptyLinePlaceholder":369},[166,1311,1312,1314,1317,1319,1322,1324,1326],{"class":168,"line":482},[166,1313,401],{"class":391},[166,1315,1316],{"class":311}," requestURL",[166,1318,407],{"class":307},[166,1320,1321],{"class":199}," getRequestURL",[166,1323,385],{"class":423},[166,1325,1231],{"class":311},[166,1327,678],{"class":423},[166,1329,1330,1332,1335,1337,1339,1341,1343,1345,1347],{"class":168,"line":500},[166,1331,401],{"class":391},[166,1333,1334],{"class":311}," id",[166,1336,407],{"class":307},[166,1338,1158],{"class":199},[166,1340,385],{"class":423},[166,1342,1040],{"class":311},[166,1344,35],{"class":307},[166,1346,551],{"class":311},[166,1348,678],{"class":423},[166,1350,1351,1353,1356,1358,1361,1364,1366,1368,1371,1374,1377,1380,1382,1384,1386,1388],{"class":168,"line":536},[166,1352,401],{"class":391},[166,1354,1355],{"class":311}," shortenURL",[166,1357,407],{"class":307},[166,1359,1360],{"class":307}," new",[166,1362,1363],{"class":199}," URL",[166,1365,385],{"class":423},[166,1367,938],{"class":307},[166,1369,1370],{"class":176},"\u002F",[166,1372,1373],{"class":307},"${",[166,1375,1376],{"class":311},"id",[166,1378,1379],{"class":307},"}`",[166,1381,315],{"class":307},[166,1383,1316],{"class":311},[166,1385,903],{"class":423},[166,1387,35],{"class":307},[166,1389,1390],{"class":311},"href\n",[166,1392,1393],{"class":168,"line":594},[166,1394,370],{"emptyLinePlaceholder":369},[166,1396,1397,1400,1403,1405,1407,1410,1412,1414,1416,1419,1421,1423,1425,1427,1429,1431],{"class":168,"line":624},[166,1398,1399],{"class":303},"  await",[166,1401,1402],{"class":199}," useStorage",[166,1404,385],{"class":423},[166,1406,338],{"class":307},[166,1408,1409],{"class":176},"data",[166,1411,338],{"class":307},[166,1413,903],{"class":423},[166,1415,35],{"class":307},[166,1417,1418],{"class":199},"setItem",[166,1420,385],{"class":423},[166,1422,1376],{"class":311},[166,1424,315],{"class":307},[166,1426,688],{"class":311},[166,1428,35],{"class":307},[166,1430,551],{"class":311},[166,1432,678],{"class":423},[166,1434,1435],{"class":168,"line":633},[166,1436,370],{"emptyLinePlaceholder":369},[166,1438,1439,1441,1443,1445,1447,1449],{"class":168,"line":643},[166,1440,401],{"class":391},[166,1442,404],{"class":311},[166,1444,407],{"class":307},[166,1446,410],{"class":307},[166,1448,392],{"class":391},[166,1450,395],{"class":307},[166,1452,1453,1455],{"class":168,"line":649},[166,1454,420],{"class":303},[166,1456,424],{"class":423},[166,1458,1459,1461,1463],{"class":168,"line":655},[166,1460,430],{"class":307},[166,1462,433],{"class":423},[166,1464,436],{"class":307},[166,1466,1467,1469,1471],{"class":168,"line":681},[166,1468,442],{"class":307},[166,1470,445],{"class":172},[166,1472,436],{"class":307},[166,1474,1475,1477,1479,1481,1484,1486,1488],{"class":168,"line":714},[166,1476,453],{"class":307},[166,1478,456],{"class":423},[166,1480,459],{"class":307},[166,1482,1483],{"class":311},"Created",[166,1485,465],{"class":307},[166,1487,456],{"class":423},[166,1489,436],{"class":307},[166,1491,1492,1494,1496],{"class":168,"line":719},[166,1493,475],{"class":307},[166,1495,445],{"class":172},[166,1497,436],{"class":307},[166,1499,1500,1502,1504,1506,1509,1511,1513],{"class":168,"line":736},[166,1501,442],{"class":307},[166,1503,144],{"class":423},[166,1505,459],{"class":307},[166,1507,1508],{"class":311},"Created and Ready",[166,1510,465],{"class":307},[166,1512,144],{"class":423},[166,1514,436],{"class":307},[166,1516,1517,1519],{"class":168,"line":745},[166,1518,442],{"class":307},[166,1520,1521],{"class":423},"input\n",[166,1523,1524,1527,1529,1531,1534],{"class":168,"line":753},[166,1525,1526],{"class":391},"          type",[166,1528,511],{"class":307},[166,1530,514],{"class":307},[166,1532,1533],{"class":176},"text",[166,1535,1536],{"class":307},"\"\n",[166,1538,1539,1542,1545,1548],{"class":168,"line":761},[166,1540,1541],{"class":391},"          value",[166,1543,1544],{"class":307},"={",[166,1546,1547],{"class":311},"shortenURL",[166,1549,877],{"class":307},[166,1551,1552],{"class":168,"line":766},[166,1553,1554],{"class":391},"          autofocus\n",[166,1556,1557],{"class":168,"line":783},[166,1558,966],{"class":307},[166,1560,1561,1563,1565],{"class":168,"line":791},[166,1562,636],{"class":307},[166,1564,433],{"class":423},[166,1566,436],{"class":307},[166,1568,1569],{"class":168,"line":798},[166,1570,646],{"class":423},[166,1572,1574],{"class":168,"line":1573},31,[166,1575,652],{"class":307},[166,1577,1579],{"class":168,"line":1578},32,[166,1580,370],{"emptyLinePlaceholder":369},[166,1582,1584,1586,1588,1590,1592,1594,1596,1598,1600],{"class":168,"line":1583},33,[166,1585,401],{"class":391},[166,1587,660],{"class":311},[166,1589,407],{"class":307},[166,1591,323],{"class":199},[166,1593,385],{"class":423},[166,1595,669],{"class":307},[166,1597,672],{"class":172},[166,1599,675],{"class":307},[166,1601,678],{"class":423},[166,1603,1605,1607,1609,1611,1613,1616,1618,1620,1622,1624,1626,1628,1630,1632,1634],{"class":168,"line":1604},34,[166,1606,401],{"class":391},[166,1608,308],{"class":307},[166,1610,688],{"class":423},[166,1612,154],{"class":307},[166,1614,1615],{"class":311}," nanoBody",[166,1617,315],{"class":307},[166,1619,693],{"class":311},[166,1621,326],{"class":307},[166,1623,407],{"class":307},[166,1625,318],{"class":311},[166,1627,35],{"class":307},[166,1629,704],{"class":199},[166,1631,385],{"class":423},[166,1633,709],{"class":311},[166,1635,678],{"class":423},[166,1637,1639],{"class":168,"line":1638},35,[166,1640,370],{"emptyLinePlaceholder":369},[166,1642,1644,1646,1648,1650],{"class":168,"line":1643},36,[166,1645,769],{"class":303},[166,1647,351],{"class":199},[166,1649,385],{"class":423},[166,1651,733],{"class":307},[166,1653,1655,1657,1659,1661],{"class":168,"line":1654},37,[166,1656,739],{"class":423},[166,1658,154],{"class":307},[166,1660,1615],{"class":311},[166,1662,742],{"class":307},[166,1664,1666,1668],{"class":168,"line":1665},38,[166,1667,748],{"class":311},[166,1669,742],{"class":307},[166,1671,1673,1675],{"class":168,"line":1672},39,[166,1674,756],{"class":307},[166,1676,678],{"class":423},[166,1678,1680,1682],{"class":168,"line":1679},40,[166,1681,801],{"class":307},[166,1683,678],{"class":311},[10,1685,1686,1687,1690,1691,1694,1695,1697],{},"In this segment, we utilize the ",[163,1688,1689],{},"readValidatedBody"," function to ",[14,1692,1693],{},"validate the request body",". This assures that the ",[163,1696,551],{}," field is a legitimate URL, throwing an error otherwise.",[10,1699,1700,1701,1704,1705,1708,1709,35],{},"We retrieve the ",[14,1702,1703],{},"request URL"," using the ",[163,1706,1707],{},"getRequestURL"," function from ",[163,1710,268],{},[10,1712,827,1713,1715,1716,1708,1718,1720],{},[14,1714,114],{}," is created from the body URL using the ",[163,1717,114],{},[14,1719,110],{},", ensuring consistent hash generation to avoid collisions.",[10,1722,1723,1724,1704,1726,1708,1729,1731,1732,1734],{},"The URL is stored in the ",[14,1725,63],{},[163,1727,1728],{},"useStorage",[14,1730,94],{},", employing the ",[163,1733,1409],{}," namespace to store the URLs, pre-configured for our use.",[1736,1737,1740,1743,1750,1851,1873],"callout-read-more",{"text":1738,"href":1739},"KV Storage","https:\u002F\u002Fnitro.unjs.io\u002Fguide\u002Fstorage#usage",[10,1741,1742],{},"At the end of this event handler, a route is returned with the shortened URL that users can copy and utilize.",[10,1744,1745,1746,1749],{},"For development, everything functions correctly, but we must update this configuration, ",[163,1747,1748],{},"nitro.config.ts",", for the production environment.",[156,1751,1753],{"className":834,"code":1752,"filename":1748,"language":837,"meta":161,"style":161},"export default defineNitroConfig({\n  srcDir: 'server',\n  $production: {\n    storage: { data: { driver: 'cloudflare-kv-binding', binding: 'url-shortener' } },\n  },\n})\n",[163,1754,1755,1768,1784,1793,1840,1845],{"__ignoreMap":161},[166,1756,1757,1759,1761,1764,1766],{"class":168,"line":169},[166,1758,376],{"class":303},[166,1760,379],{"class":303},[166,1762,1763],{"class":199}," defineNitroConfig",[166,1765,385],{"class":311},[166,1767,733],{"class":307},[166,1769,1770,1773,1775,1777,1780,1782],{"class":168,"line":205},[166,1771,1772],{"class":423},"  srcDir",[166,1774,154],{"class":307},[166,1776,332],{"class":307},[166,1778,1779],{"class":176},"server",[166,1781,338],{"class":307},[166,1783,742],{"class":307},[166,1785,1786,1789,1791],{"class":168,"line":366},[166,1787,1788],{"class":423},"  $production",[166,1790,154],{"class":307},[166,1792,395],{"class":307},[166,1794,1795,1798,1800,1802,1805,1807,1809,1812,1814,1816,1819,1821,1823,1826,1828,1830,1833,1835,1837],{"class":168,"line":373},[166,1796,1797],{"class":423},"    storage",[166,1799,154],{"class":307},[166,1801,308],{"class":307},[166,1803,1804],{"class":423}," data",[166,1806,154],{"class":307},[166,1808,308],{"class":307},[166,1810,1811],{"class":423}," driver",[166,1813,154],{"class":307},[166,1815,332],{"class":307},[166,1817,1818],{"class":176},"cloudflare-kv-binding",[166,1820,338],{"class":307},[166,1822,315],{"class":307},[166,1824,1825],{"class":423}," binding",[166,1827,154],{"class":307},[166,1829,332],{"class":307},[166,1831,1832],{"class":176},"url-shortener",[166,1834,338],{"class":307},[166,1836,326],{"class":307},[166,1838,1839],{"class":307}," },\n",[166,1841,1842],{"class":168,"line":398},[166,1843,1844],{"class":307},"  },\n",[166,1846,1847,1849],{"class":168,"line":417},[166,1848,801],{"class":307},[166,1850,678],{"class":311},[10,1852,1853,1854,1856,1857,1859,1860,1862,1863,1866,1867,1870,1871,35],{},"In this configuration, we define the ",[163,1855,1409],{}," namespace, as in development, to use the ",[14,1858,1818],{}," driver and the ",[163,1861,1832],{}," binding, specified under the key ",[163,1864,1865],{},"$production",". This indicates that the configuration applies solely to the ",[14,1868,1869],{},"production environment"," for accessing the ",[14,1872,73],{},[1736,1874,1877,1881,1890,2089,2109,2113,2123],{"text":1875,"href":1876},"Environment-specific configuration","https:\u002F\u002Fgithub.com\u002Funjs\u002Fc12#environment-specific-configuration",[268,1878,1880],{"id":1879},"redirecting-to-the-original-url","Redirecting to the original URL",[10,1882,1883,1884,1887,1888,1117],{},"Finally, we need to establish a route to handle shortened URLs and redirect users to the primary URL. This route is named ",[163,1885,1886],{},"[id].get.ts"," and is situated in the ",[163,1889,285],{},[156,1891,1894],{"className":834,"code":1892,"filename":1893,"language":837,"meta":161,"style":161},"export default defineEventHandler(async (event) => {\n  const id = getRouterParam(event, 'short')\n\n  const value = await useStorage\u003Cstring>('data').getItem(id)\n\n  if (!value) {\n    throw createError({\n      statusCode: 404,\n      statusMessage: 'Not Found',\n    })\n  }\n\n  return sendRedirect(event, value)\n})\n","server\u002Froutes\u002F[id].get.ts",[163,1895,1896,1918,1944,1948,1988,1992,2010,2022,2035,2051,2058,2062,2066,2083],{"__ignoreMap":161},[166,1897,1898,1900,1902,1904,1906,1908,1910,1912,1914,1916],{"class":168,"line":169},[166,1899,376],{"class":303},[166,1901,379],{"class":303},[166,1903,772],{"class":199},[166,1905,385],{"class":311},[166,1907,1225],{"class":391},[166,1909,1228],{"class":307},[166,1911,1231],{"class":895},[166,1913,903],{"class":307},[166,1915,392],{"class":391},[166,1917,395],{"class":307},[166,1919,1920,1922,1924,1926,1929,1931,1933,1935,1937,1940,1942],{"class":168,"line":205},[166,1921,401],{"class":391},[166,1923,1334],{"class":311},[166,1925,407],{"class":307},[166,1927,1928],{"class":199}," getRouterParam",[166,1930,385],{"class":423},[166,1932,1231],{"class":311},[166,1934,315],{"class":307},[166,1936,332],{"class":307},[166,1938,1939],{"class":176},"short",[166,1941,338],{"class":307},[166,1943,678],{"class":423},[166,1945,1946],{"class":168,"line":366},[166,1947,370],{"emptyLinePlaceholder":369},[166,1949,1950,1952,1955,1957,1959,1961,1963,1965,1967,1969,1971,1973,1975,1977,1979,1982,1984,1986],{"class":168,"line":373},[166,1951,401],{"class":391},[166,1953,1954],{"class":311}," value",[166,1956,407],{"class":307},[166,1958,1248],{"class":303},[166,1960,1402],{"class":199},[166,1962,669],{"class":307},[166,1964,1282],{"class":172},[166,1966,459],{"class":307},[166,1968,385],{"class":423},[166,1970,338],{"class":307},[166,1972,1409],{"class":176},[166,1974,338],{"class":307},[166,1976,903],{"class":423},[166,1978,35],{"class":307},[166,1980,1981],{"class":199},"getItem",[166,1983,385],{"class":423},[166,1985,1376],{"class":311},[166,1987,678],{"class":423},[166,1989,1990],{"class":168,"line":398},[166,1991,370],{"emptyLinePlaceholder":369},[166,1993,1994,1997,1999,2002,2005,2008],{"class":168,"line":417},[166,1995,1996],{"class":303},"  if",[166,1998,1228],{"class":423},[166,2000,2001],{"class":307},"!",[166,2003,2004],{"class":311},"value",[166,2006,2007],{"class":423},") ",[166,2009,733],{"class":307},[166,2011,2012,2015,2018,2020],{"class":168,"line":427},[166,2013,2014],{"class":303},"    throw",[166,2016,2017],{"class":199}," createError",[166,2019,385],{"class":423},[166,2021,733],{"class":307},[166,2023,2024,2027,2029,2033],{"class":168,"line":439},[166,2025,2026],{"class":423},"      statusCode",[166,2028,154],{"class":307},[166,2030,2032],{"class":2031},"sbssI"," 404",[166,2034,742],{"class":307},[166,2036,2037,2040,2042,2044,2047,2049],{"class":168,"line":450},[166,2038,2039],{"class":423},"      statusMessage",[166,2041,154],{"class":307},[166,2043,332],{"class":307},[166,2045,2046],{"class":176},"Not Found",[166,2048,338],{"class":307},[166,2050,742],{"class":307},[166,2052,2053,2056],{"class":168,"line":472},[166,2054,2055],{"class":307},"    }",[166,2057,678],{"class":423},[166,2059,2060],{"class":168,"line":482},[166,2061,652],{"class":307},[166,2063,2064],{"class":168,"line":500},[166,2065,370],{"emptyLinePlaceholder":369},[166,2067,2068,2070,2073,2075,2077,2079,2081],{"class":168,"line":536},[166,2069,769],{"class":303},[166,2071,2072],{"class":199}," sendRedirect",[166,2074,385],{"class":423},[166,2076,1231],{"class":311},[166,2078,315],{"class":307},[166,2080,1954],{"class":311},[166,2082,678],{"class":423},[166,2084,2085,2087],{"class":168,"line":594},[166,2086,801],{"class":307},[166,2088,678],{"class":311},[10,2090,2091,2092,2094,2095,2097,2098,2101,2102,2104,2105,2108],{},"Here, we simply obtain the ",[163,2093,1376],{}," from the router parameters and use ",[163,2096,1728],{}," to fetch the ",[14,2099,2100],{},"URL"," from the ",[14,2103,63],{},". If the URL does not exist, a ",[14,2106,2107],{},"404 error"," is thrown. Otherwise, the user is redirected to the original URL.",[144,2110,2112],{"id":2111},"deploying-the-url-shortener","Deploying the URL shortener",[10,2114,2115,2116,2119,2120,35],{},"Congratulations! We have successfully created a basic URL shortener using Nitro. The next step is to ",[14,2117,2118],{},"deploy"," it on Cloudflare Pages. Rest assured, it's straightforward thanks to the ",[14,2121,2122],{},"zero-config deployment providers",[1736,2124,2127,2136,2155,2160,2177,2192,2199,2203,2213,2983,2989,3012,3016,3030,3038],{"text":2125,"href":2126},"Zero Config Providers","https:\u002F\u002Fnitro.unjs.io\u002Fdeploy#zero-config-providers",[2128,2129,2130],"blockquote",{},[10,2131,2132,2135],{},[166,2133,2134],{},"!WARNING","\nA Cloudflare account and a GitHub repository are required to continue with the deployment.\nInitially, log in to the Cloudflare dashboard and select an account. Navigate to Account Home, choose Workers & Pages, and create a new application using the button at the top right. Select the Pages tab and follow the instructions to establish a GitHub connection.",[10,2137,2138,2139,2142,2143,2146,2147,2150,2151,2154],{},"Select the repository, add the build command ",[163,2140,2141],{},"npm run build",", specify the output directory ",[163,2144,2145],{},"dist",", include an environment variable ",[163,2148,2149],{},"NODE_ENV"," with the value ",[163,2152,2153],{},"production",", and save and deploy.",[10,2156,2157,2158,35],{},"Now, let's wait for the deployment to complete. Once done, you can access the URL shortener using the URL provided by ",[14,2159,34],{},[10,2161,2162,2165,2166,2169,2170,2173,2174,2176],{},[14,2163,2164],{},"However",", it may not function adequately initially, as a ",[14,2167,2168],{},"KV namespace"," has not been bound. This requires manual binding. Go to the project settings, select functions, and scroll until you find ",[14,2171,2172],{},"KV namespace bindings",". Add a binding with the name ",[163,2175,1832],{}," and choose the desired namespace.",[2128,2178,2179],{},[10,2180,2181,2184,2185,2187,2188,2191],{},[166,2182,2183],{},"!NOTE","\nCreating a ",[14,2186,2168],{}," is achievable in the ",[14,2189,2190],{},"Workers & Pages"," section.\nRe-deploy your application to account for the new KV namespace binding. Once completed, the URL shortener will be fully functional. ✨",[2128,2193,2194],{},[10,2195,2196,2198],{},[166,2197,2183],{},"\nIf you do not intend to maintain the project, remember to delete it from the Cloudflare Pages dashboard to avoid unnecessary expenses.",[144,2200,2202],{"id":2201},"enhancements-and-future-considerations","Enhancements and future considerations",[10,2204,2205,2206,2209,2210,2212],{},"To enhance security, consider adding simple ",[14,2207,2208],{},"CSRF protection"," using the handler object syntax. Modify the ",[163,2211,1113],{}," file to include a pre-request handler for origin header verification.",[156,2214,2216],{"className":293,"code":2215,"filename":1121,"language":296,"meta":161,"style":161},"import { h, Helmet, renderSSR } from 'nano-jsx'\nimport { hash } from 'ohash'\nimport { z } from 'zod'\nimport { withTemplate } from '..\u002Fresources\u002Ftemplate'\n\nexport default defineEventHandler({\n  onBeforeResponse: async (event) => {\n    const requestURL = getRequestURL(event).origin\n    const origin = getRequestHeader(event, 'origin')\n\n    if (!origin) {\n      throw createError({\n        statusCode: 400,\n        statusMessage: 'Bad Request',\n      })\n    }\n\n    if (origin !== requestURL) {\n      throw createError({\n        statusCode: 403,\n        statusMessage: 'Forbidden',\n      })\n    }\n  },\n  handler: async (event) => {\n    const body = await readValidatedBody(event, z.object({\n      url: z.string().url(),\n    }).parse)\n\n    const requestURL = getRequestURL(event)\n    const id = hash(body.url)\n    const shortenURL = new URL(`\u002F${id}`, requestURL).href\n\n    await useStorage('data').setItem(id, body.url)\n\n    const App = () => {\n      return (\n        \u003Cdiv>\n          \u003CHelmet>\n            \u003Ctitle>Created\u003C\u002Ftitle>\n          \u003C\u002FHelmet>\n          \u003Ch2>Created and Ready\u003C\u002Fh2>\n          \u003Cinput\n            type=\"text\"\n            value={shortenURL}\n            autofocus\n          \u002F>\n        \u003C\u002Fdiv>\n      )\n    }\n\n    const app = renderSSR(\u003CApp \u002F>)\n    const { body: nanoBody, head } = Helmet.SSR(app)\n\n    return withTemplate({\n      body: nanoBody,\n      head,\n    })\n  },\n})\n",[163,2217,2218,2244,2262,2280,2298,2302,2314,2334,2356,2383,2387,2402,2413,2425,2441,2448,2453,2457,2474,2484,2495,2510,2516,2520,2524,2543,2571,2594,2606,2610,2626,2646,2680,2684,2719,2723,2737,2744,2752,2760,2777,2787,2804,2811,2825,2837,2843,2849,2858,2864,2869,2874,2895,2928,2933,2944,2956,2964,2971,2976],{"__ignoreMap":161},[166,2219,2220,2222,2224,2226,2228,2230,2232,2234,2236,2238,2240,2242],{"class":168,"line":169},[166,2221,304],{"class":303},[166,2223,308],{"class":307},[166,2225,312],{"class":311},[166,2227,315],{"class":307},[166,2229,318],{"class":311},[166,2231,315],{"class":307},[166,2233,323],{"class":311},[166,2235,326],{"class":307},[166,2237,329],{"class":303},[166,2239,332],{"class":307},[166,2241,335],{"class":176},[166,2243,363],{"class":307},[166,2245,2246,2248,2250,2252,2254,2256,2258,2260],{"class":168,"line":205},[166,2247,304],{"class":303},[166,2249,308],{"class":307},[166,2251,1158],{"class":311},[166,2253,326],{"class":307},[166,2255,329],{"class":303},[166,2257,332],{"class":307},[166,2259,110],{"class":176},[166,2261,363],{"class":307},[166,2263,2264,2266,2268,2270,2272,2274,2276,2278],{"class":168,"line":366},[166,2265,304],{"class":303},[166,2267,308],{"class":307},[166,2269,1177],{"class":311},[166,2271,326],{"class":307},[166,2273,329],{"class":303},[166,2275,332],{"class":307},[166,2277,1083],{"class":176},[166,2279,363],{"class":307},[166,2281,2282,2284,2286,2288,2290,2292,2294,2296],{"class":168,"line":373},[166,2283,304],{"class":303},[166,2285,308],{"class":307},[166,2287,351],{"class":311},[166,2289,326],{"class":307},[166,2291,329],{"class":303},[166,2293,332],{"class":307},[166,2295,360],{"class":176},[166,2297,363],{"class":307},[166,2299,2300],{"class":168,"line":398},[166,2301,370],{"emptyLinePlaceholder":369},[166,2303,2304,2306,2308,2310,2312],{"class":168,"line":417},[166,2305,376],{"class":303},[166,2307,379],{"class":303},[166,2309,772],{"class":199},[166,2311,385],{"class":311},[166,2313,733],{"class":307},[166,2315,2316,2319,2321,2324,2326,2328,2330,2332],{"class":168,"line":427},[166,2317,2318],{"class":199},"  onBeforeResponse",[166,2320,154],{"class":307},[166,2322,2323],{"class":391}," async",[166,2325,1228],{"class":307},[166,2327,1231],{"class":895},[166,2329,903],{"class":307},[166,2331,392],{"class":391},[166,2333,395],{"class":307},[166,2335,2336,2339,2341,2343,2345,2347,2349,2351,2353],{"class":168,"line":439},[166,2337,2338],{"class":391},"    const",[166,2340,1316],{"class":311},[166,2342,407],{"class":307},[166,2344,1321],{"class":199},[166,2346,385],{"class":423},[166,2348,1231],{"class":311},[166,2350,903],{"class":423},[166,2352,35],{"class":307},[166,2354,2355],{"class":311},"origin\n",[166,2357,2358,2360,2363,2365,2368,2370,2372,2374,2376,2379,2381],{"class":168,"line":450},[166,2359,2338],{"class":391},[166,2361,2362],{"class":311}," origin",[166,2364,407],{"class":307},[166,2366,2367],{"class":199}," getRequestHeader",[166,2369,385],{"class":423},[166,2371,1231],{"class":311},[166,2373,315],{"class":307},[166,2375,332],{"class":307},[166,2377,2378],{"class":176},"origin",[166,2380,338],{"class":307},[166,2382,678],{"class":423},[166,2384,2385],{"class":168,"line":472},[166,2386,370],{"emptyLinePlaceholder":369},[166,2388,2389,2392,2394,2396,2398,2400],{"class":168,"line":482},[166,2390,2391],{"class":303},"    if",[166,2393,1228],{"class":423},[166,2395,2001],{"class":307},[166,2397,2378],{"class":311},[166,2399,2007],{"class":423},[166,2401,733],{"class":307},[166,2403,2404,2407,2409,2411],{"class":168,"line":500},[166,2405,2406],{"class":303},"      throw",[166,2408,2017],{"class":199},[166,2410,385],{"class":423},[166,2412,733],{"class":307},[166,2414,2415,2418,2420,2423],{"class":168,"line":536},[166,2416,2417],{"class":423},"        statusCode",[166,2419,154],{"class":307},[166,2421,2422],{"class":2031}," 400",[166,2424,742],{"class":307},[166,2426,2427,2430,2432,2434,2437,2439],{"class":168,"line":594},[166,2428,2429],{"class":423},"        statusMessage",[166,2431,154],{"class":307},[166,2433,332],{"class":307},[166,2435,2436],{"class":176},"Bad Request",[166,2438,338],{"class":307},[166,2440,742],{"class":307},[166,2442,2443,2446],{"class":168,"line":624},[166,2444,2445],{"class":307},"      }",[166,2447,678],{"class":423},[166,2449,2450],{"class":168,"line":633},[166,2451,2452],{"class":307},"    }\n",[166,2454,2455],{"class":168,"line":643},[166,2456,370],{"emptyLinePlaceholder":369},[166,2458,2459,2461,2463,2465,2468,2470,2472],{"class":168,"line":649},[166,2460,2391],{"class":303},[166,2462,1228],{"class":423},[166,2464,2378],{"class":311},[166,2466,2467],{"class":307}," !==",[166,2469,1316],{"class":311},[166,2471,2007],{"class":423},[166,2473,733],{"class":307},[166,2475,2476,2478,2480,2482],{"class":168,"line":655},[166,2477,2406],{"class":303},[166,2479,2017],{"class":199},[166,2481,385],{"class":423},[166,2483,733],{"class":307},[166,2485,2486,2488,2490,2493],{"class":168,"line":681},[166,2487,2417],{"class":423},[166,2489,154],{"class":307},[166,2491,2492],{"class":2031}," 403",[166,2494,742],{"class":307},[166,2496,2497,2499,2501,2503,2506,2508],{"class":168,"line":714},[166,2498,2429],{"class":423},[166,2500,154],{"class":307},[166,2502,332],{"class":307},[166,2504,2505],{"class":176},"Forbidden",[166,2507,338],{"class":307},[166,2509,742],{"class":307},[166,2511,2512,2514],{"class":168,"line":719},[166,2513,2445],{"class":307},[166,2515,678],{"class":423},[166,2517,2518],{"class":168,"line":736},[166,2519,2452],{"class":307},[166,2521,2522],{"class":168,"line":745},[166,2523,1844],{"class":307},[166,2525,2526,2529,2531,2533,2535,2537,2539,2541],{"class":168,"line":753},[166,2527,2528],{"class":199},"  handler",[166,2530,154],{"class":307},[166,2532,2323],{"class":391},[166,2534,1228],{"class":307},[166,2536,1231],{"class":895},[166,2538,903],{"class":307},[166,2540,392],{"class":391},[166,2542,395],{"class":307},[166,2544,2545,2547,2549,2551,2553,2555,2557,2559,2561,2563,2565,2567,2569],{"class":168,"line":761},[166,2546,2338],{"class":391},[166,2548,688],{"class":311},[166,2550,407],{"class":307},[166,2552,1248],{"class":303},[166,2554,1251],{"class":199},[166,2556,385],{"class":423},[166,2558,1231],{"class":311},[166,2560,315],{"class":307},[166,2562,1177],{"class":311},[166,2564,35],{"class":307},[166,2566,1264],{"class":199},[166,2568,385],{"class":423},[166,2570,733],{"class":307},[166,2572,2573,2576,2578,2580,2582,2584,2586,2588,2590,2592],{"class":168,"line":766},[166,2574,2575],{"class":423},"      url",[166,2577,154],{"class":307},[166,2579,1177],{"class":311},[166,2581,35],{"class":307},[166,2583,1282],{"class":199},[166,2585,388],{"class":423},[166,2587,35],{"class":307},[166,2589,551],{"class":199},[166,2591,388],{"class":423},[166,2593,742],{"class":307},[166,2595,2596,2598,2600,2602,2604],{"class":168,"line":783},[166,2597,2055],{"class":307},[166,2599,903],{"class":423},[166,2601,35],{"class":307},[166,2603,1303],{"class":311},[166,2605,678],{"class":423},[166,2607,2608],{"class":168,"line":791},[166,2609,370],{"emptyLinePlaceholder":369},[166,2611,2612,2614,2616,2618,2620,2622,2624],{"class":168,"line":798},[166,2613,2338],{"class":391},[166,2615,1316],{"class":311},[166,2617,407],{"class":307},[166,2619,1321],{"class":199},[166,2621,385],{"class":423},[166,2623,1231],{"class":311},[166,2625,678],{"class":423},[166,2627,2628,2630,2632,2634,2636,2638,2640,2642,2644],{"class":168,"line":1573},[166,2629,2338],{"class":391},[166,2631,1334],{"class":311},[166,2633,407],{"class":307},[166,2635,1158],{"class":199},[166,2637,385],{"class":423},[166,2639,1040],{"class":311},[166,2641,35],{"class":307},[166,2643,551],{"class":311},[166,2645,678],{"class":423},[166,2647,2648,2650,2652,2654,2656,2658,2660,2662,2664,2666,2668,2670,2672,2674,2676,2678],{"class":168,"line":1578},[166,2649,2338],{"class":391},[166,2651,1355],{"class":311},[166,2653,407],{"class":307},[166,2655,1360],{"class":307},[166,2657,1363],{"class":199},[166,2659,385],{"class":423},[166,2661,938],{"class":307},[166,2663,1370],{"class":176},[166,2665,1373],{"class":307},[166,2667,1376],{"class":311},[166,2669,1379],{"class":307},[166,2671,315],{"class":307},[166,2673,1316],{"class":311},[166,2675,903],{"class":423},[166,2677,35],{"class":307},[166,2679,1390],{"class":311},[166,2681,2682],{"class":168,"line":1583},[166,2683,370],{"emptyLinePlaceholder":369},[166,2685,2686,2689,2691,2693,2695,2697,2699,2701,2703,2705,2707,2709,2711,2713,2715,2717],{"class":168,"line":1604},[166,2687,2688],{"class":303},"    await",[166,2690,1402],{"class":199},[166,2692,385],{"class":423},[166,2694,338],{"class":307},[166,2696,1409],{"class":176},[166,2698,338],{"class":307},[166,2700,903],{"class":423},[166,2702,35],{"class":307},[166,2704,1418],{"class":199},[166,2706,385],{"class":423},[166,2708,1376],{"class":311},[166,2710,315],{"class":307},[166,2712,688],{"class":311},[166,2714,35],{"class":307},[166,2716,551],{"class":311},[166,2718,678],{"class":423},[166,2720,2721],{"class":168,"line":1638},[166,2722,370],{"emptyLinePlaceholder":369},[166,2724,2725,2727,2729,2731,2733,2735],{"class":168,"line":1643},[166,2726,2338],{"class":391},[166,2728,404],{"class":311},[166,2730,407],{"class":307},[166,2732,410],{"class":307},[166,2734,392],{"class":391},[166,2736,395],{"class":307},[166,2738,2739,2742],{"class":168,"line":1654},[166,2740,2741],{"class":303},"      return",[166,2743,424],{"class":423},[166,2745,2746,2748,2750],{"class":168,"line":1665},[166,2747,442],{"class":307},[166,2749,433],{"class":423},[166,2751,436],{"class":307},[166,2753,2754,2756,2758],{"class":168,"line":1672},[166,2755,453],{"class":307},[166,2757,445],{"class":172},[166,2759,436],{"class":307},[166,2761,2762,2765,2767,2769,2771,2773,2775],{"class":168,"line":1679},[166,2763,2764],{"class":307},"            \u003C",[166,2766,456],{"class":423},[166,2768,459],{"class":307},[166,2770,1483],{"class":311},[166,2772,465],{"class":307},[166,2774,456],{"class":423},[166,2776,436],{"class":307},[166,2778,2780,2783,2785],{"class":168,"line":2779},41,[166,2781,2782],{"class":307},"          \u003C\u002F",[166,2784,445],{"class":172},[166,2786,436],{"class":307},[166,2788,2790,2792,2794,2796,2798,2800,2802],{"class":168,"line":2789},42,[166,2791,453],{"class":307},[166,2793,144],{"class":423},[166,2795,459],{"class":307},[166,2797,1508],{"class":311},[166,2799,465],{"class":307},[166,2801,144],{"class":423},[166,2803,436],{"class":307},[166,2805,2807,2809],{"class":168,"line":2806},43,[166,2808,453],{"class":307},[166,2810,1521],{"class":423},[166,2812,2814,2817,2819,2821,2823],{"class":168,"line":2813},44,[166,2815,2816],{"class":391},"            type",[166,2818,511],{"class":307},[166,2820,514],{"class":307},[166,2822,1533],{"class":176},[166,2824,1536],{"class":307},[166,2826,2828,2831,2833,2835],{"class":168,"line":2827},45,[166,2829,2830],{"class":391},"            value",[166,2832,1544],{"class":307},[166,2834,1547],{"class":311},[166,2836,877],{"class":307},[166,2838,2840],{"class":168,"line":2839},46,[166,2841,2842],{"class":391},"            autofocus\n",[166,2844,2846],{"class":168,"line":2845},47,[166,2847,2848],{"class":307},"          \u002F>\n",[166,2850,2852,2854,2856],{"class":168,"line":2851},48,[166,2853,475],{"class":307},[166,2855,433],{"class":423},[166,2857,436],{"class":307},[166,2859,2861],{"class":168,"line":2860},49,[166,2862,2863],{"class":423},"      )\n",[166,2865,2867],{"class":168,"line":2866},50,[166,2868,2452],{"class":307},[166,2870,2872],{"class":168,"line":2871},51,[166,2873,370],{"emptyLinePlaceholder":369},[166,2875,2877,2879,2881,2883,2885,2887,2889,2891,2893],{"class":168,"line":2876},52,[166,2878,2338],{"class":391},[166,2880,660],{"class":311},[166,2882,407],{"class":307},[166,2884,323],{"class":199},[166,2886,385],{"class":423},[166,2888,669],{"class":307},[166,2890,672],{"class":172},[166,2892,675],{"class":307},[166,2894,678],{"class":423},[166,2896,2898,2900,2902,2904,2906,2908,2910,2912,2914,2916,2918,2920,2922,2924,2926],{"class":168,"line":2897},53,[166,2899,2338],{"class":391},[166,2901,308],{"class":307},[166,2903,688],{"class":423},[166,2905,154],{"class":307},[166,2907,1615],{"class":311},[166,2909,315],{"class":307},[166,2911,693],{"class":311},[166,2913,326],{"class":307},[166,2915,407],{"class":307},[166,2917,318],{"class":311},[166,2919,35],{"class":307},[166,2921,704],{"class":199},[166,2923,385],{"class":423},[166,2925,709],{"class":311},[166,2927,678],{"class":423},[166,2929,2931],{"class":168,"line":2930},54,[166,2932,370],{"emptyLinePlaceholder":369},[166,2934,2936,2938,2940,2942],{"class":168,"line":2935},55,[166,2937,420],{"class":303},[166,2939,351],{"class":199},[166,2941,385],{"class":423},[166,2943,733],{"class":307},[166,2945,2947,2950,2952,2954],{"class":168,"line":2946},56,[166,2948,2949],{"class":423},"      body",[166,2951,154],{"class":307},[166,2953,1615],{"class":311},[166,2955,742],{"class":307},[166,2957,2959,2962],{"class":168,"line":2958},57,[166,2960,2961],{"class":311},"      head",[166,2963,742],{"class":307},[166,2965,2967,2969],{"class":168,"line":2966},58,[166,2968,2055],{"class":307},[166,2970,678],{"class":423},[166,2972,2974],{"class":168,"line":2973},59,[166,2975,1844],{"class":307},[166,2977,2979,2981],{"class":168,"line":2978},60,[166,2980,801],{"class":307},[166,2982,678],{"class":311},[10,2984,2985,2986,2988],{},"We set up a pre-request handler to check the ",[14,2987,2378],{}," header. If there is a mismatch, an error is raised; otherwise, the request proceeds to the main handler.",[2128,2990,2991],{},[10,2992,2993,2995,2996,2998,2999,3004,3005,3007,3008,3011],{},[166,2994,2183],{},"\nFor comprehensive information on ",[14,2997,2208],{},", refer to the ",[19,3000,3003],{"href":3001,"rel":3002},"https:\u002F\u002Fcheatsheetseries.owasp.org\u002Fcheatsheets\u002FCross-Site_Request_Forgery_Prevention_Cheat_Sheet.html",[23],"Cross-Site Request Forgery Prevention Cheat Sheet"," by OWASP. Validating the ",[14,3006,2378],{}," header is a basic defense approach for ",[14,3009,3010],{},"CSRF",". However, it is advisable to implement a more robust protection mechanism such as a token.",[144,3013,3015],{"id":3014},"conclusion","Conclusion",[10,3017,3018,3019,3021,3022,3024,3025,3027,3028,35],{},"Developing with ",[14,3020,26],{}," and ",[14,3023,34],{}," is straightforward and provides an exceptional developer experience due to the ability to utilize ",[14,3026,132],{}," and development storage, eliminating the need for the ",[14,3029,101],{},[10,3031,3032,3033,3021,3035,3037],{},"Enjoy your development journey with ",[14,3034,26],{},[14,3036,34],{},"! 🚀",[2128,3039,3040],{},[10,3041,3042,3044,3045,35],{},[166,3043,2183],{},"\nThe URL shortener is inspired by ",[19,3046,3049],{"href":3047,"rel":3048},"https:\u002F\u002Fgithub.com\u002Fyusukebe\u002Furl-shortener",[23],"Yusuke Wada's URL Shortener",[3051,3052,3053],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}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 .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":161,"searchDepth":205,"depth":205,"links":3055},[3056,3057],{"id":146,"depth":205,"text":147},{"id":244,"depth":205,"text":245,"children":3058},[3059,3060],{"id":270,"depth":366,"text":271},{"id":1076,"depth":366,"text":1077},"5b969fcd-0a41-4bc1-ae7e-3aca6d1f48a5","2024-03-19","Learn how to create a URL shortener using Nitro and deploy it on Cloudflare Pages, enabling effortless edge app development.","md",{},"\u002Fposts\u002Fbuild-a-url-shortener-with-nitro-on-cloudflare-pages","---\ncontentId: 5b969fcd-0a41-4bc1-ae7e-3aca6d1f48a5\ntitle: Build a URL Shortener with Nitro on Cloudflare Pages\ndescription: Learn how to create a URL shortener using Nitro and deploy it on Cloudflare Pages, enabling effortless edge app development.\ndate: 2024-03-19\n---\n\nIn this article, we will develop a **URL shortener** utilizing [**Nitro**](https:\u002F\u002Fnitro.unjs.io) and deploy it on [**Cloudflare Pages**](https:\u002F\u002Fpages.cloudflare.com\u002F).\n\nThe source code is accessible on \u003CGitHubLink repo=\"barbapapazes\u002Furl-shortener\" \u002F>.\n\n**Nitro** represents a new generation of **server toolkit**. It empowers us to construct web servers with all necessary functionalities and deploy them at our convenience.\n\n**Cloudflare Pages** is a platform designed to build and host websites on the edge. It supports services like [**KV**](https:\u002F\u002Fdevelopers.cloudflare.com\u002Fkv\u002F) to create comprehensive full-stack stateful applications.\n\nOur project is a straightforward **URL shortener** that facilitates converting a long URL into a shortened version. We will leverage the **Cloudflare KV** to store the URLs and the **Nitro server** to manage requests.\n\nWe will utilize:\n\n- [**unstorage**](https:\u002F\u002Funstorage.unjs.io) to streamline the development process by abstracting the **KV** layer, eliminating the need for the **Cloudflare Wrangler CLI**.\n- [**ohash**](https:\u002F\u002Fohash.unjs.io) to generate a **hash** from the URL to prevent collisions.\n- [**nanojsx**](https:\u002F\u002Fnanojsx.io\u002F) to build the **HTML pages** using **TSX**.\n- [**pico.css**](https:\u002F\u002Fpicocss.com) for styling the application.\n\n## Project initialization\n\nFirst, create a new **Nitro project**:\n\n```bash\nnpx giget@latest nitro url-shortener\n```\n\nSubsequently, navigate to the project and install the required **dependencies**:\n\n```bash\ncd url-shortener\nnpm install\n```\n\nStart the **development server** to view the default Nitro page:\n\n```bash\nnpm run dev\n```\n\nOpen your browser and visit [http:\u002F\u002Flocalhost:3000](http:\u002F\u002Flocalhost:3000) to verify functionality.\n\n## Constructing the URL shortener\n\nInitially, install the necessary packages:\n\n```bash\nnpm install ohash nano-jsx\n```\n\n### Generate a short URL\n\nCreate a **route** named `index.get.tsx` within the `server\u002Froutes` directory. This will serve as the **home page** of our URL shortener where users can generate a shortened URL from a long one.\n\n```tsx [server\u002Froutes\u002Findex.get.tsx]\nimport { h, Helmet, renderSSR } from 'nano-jsx' \u002F\u002F the `h` is critical here\nimport { withTemplate } from '..\u002Fresources\u002Ftemplate'\n\nexport default defineLazyEventHandler(() => {\n  const App = () => {\n    return (\n      \u003Cdiv>\n        \u003CHelmet>\n          \u003Ctitle>URL Shortener with Nitro\u003C\u002Ftitle>\n        \u003C\u002FHelmet>\n        \u003Ch2>Shorten a URL\u003C\u002Fh2>\n        \u003Cform action=\"\u002Fcreate\" method=\"POST\">\n          \u003Cinput type=\"url\" name=\"url\" placeholder=\"URL to shorten\" autocomplete=\"off\" \u002F>\n          \u003Cbutton type=\"submit\">Create\u003C\u002Fbutton>\n        \u003C\u002Fform>\n      \u003C\u002Fdiv>\n    )\n  }\n  const app = renderSSR(\u003CApp \u002F>)\n  const { body, head } = Helmet.SSR(app)\n\n  const page = withTemplate({\n    body,\n    head,\n  })\n\n  return defineEventHandler(() => {\n    return page\n  })\n})\n```\n\nThis route will present a form enabling users to input a URL to shorten. Upon form submission, a **POST request** is sent to the `\u002Fcreate` route.\n\nA **lazy event handler** is utilized to generate the view only once, when a request reaches the server. The response is then **cached in-memory** and reused for future requests, reducing the computational load.\n\nThe `withTemplate` function serves as a utility that we must construct.\n\n```ts [server\u002Fresources\u002Ftemplate.tsx]\ninterface LayoutProps {\n  body: string\n  head: string[]\n}\n\nexport function withTemplate(props: LayoutProps) {\n  const { head, body } = props\n\n  return \u002F* html *\u002F`\u003Chtml>\n      \u003Chead>\n       \u003Clink\n          rel=\"stylesheet\"\n          href=\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002F@picocss\u002Fpico@2\u002Fcss\u002Fpico.min.css\"\n        \u002F>\n        ${head.join('\\n')}\n      \u003C\u002Fhead>\n      \u003Cbody>\n        \u003Cheader class=\"container\">\n          \u003Ch1>\n            \u003Ca href=\"\u002F\">URL Shortener with Nitro\u003C\u002Fa>\n          \u003C\u002Fh1>\n        \u003C\u002Fheader>\n        \u003Cmain class=\"container\">\n          ${body}\n        \u003C\u002Fmain>\n      \u003C\u002Fbody>\n    \u003C\u002Fhtml>`\n}\n```\n\nThis is a simple **string template** where we incorporate the head and body content generated by **nano-jsx**.\n\n### URL storage\n\nBefore proceeding, install `zod` to validate the request body.\n\n```bash\nnpm install zod\n```\n\nCreate the `\u002Fcreate` route to manage the **POST request** and store the URL in the **KV**. This route is labeled `create.post.tsx` and resides in the `server\u002Froutes` directory.\n\n```tsx [server\u002Froutes\u002Fcreate.post.tsx]\nimport { h, Helmet, renderSSR } from 'nano-jsx'\nimport { hash } from 'ohash'\nimport { z } from 'zod' \u002F\u002F the `h` is essential here\nimport { withTemplate } from '..\u002Fresources\u002Ftemplate'\n\nexport default defineEventHandler(async (event) => {\n  const body = await readValidatedBody(event, z.object({\n    url: z.string().url(),\n  }).parse)\n\n  const requestURL = getRequestURL(event)\n  const id = hash(body.url)\n  const shortenURL = new URL(`\u002F${id}`, requestURL).href\n\n  await useStorage('data').setItem(id, body.url)\n\n  const App = () => {\n    return (\n      \u003Cdiv>\n        \u003CHelmet>\n          \u003Ctitle>Created\u003C\u002Ftitle>\n        \u003C\u002FHelmet>\n        \u003Ch2>Created and Ready\u003C\u002Fh2>\n        \u003Cinput\n          type=\"text\"\n          value={shortenURL}\n          autofocus\n        \u002F>\n      \u003C\u002Fdiv>\n    )\n  }\n\n  const app = renderSSR(\u003CApp \u002F>)\n  const { body: nanoBody, head } = Helmet.SSR(app)\n\n  return withTemplate({\n    body: nanoBody,\n    head,\n  })\n})\n```\n\nIn this segment, we utilize the `readValidatedBody` function to **validate the request body**. This assures that the `url` field is a legitimate URL, throwing an error otherwise.\n\nWe retrieve the **request URL** using the `getRequestURL` function from `h3`.\n\nThe **hash** is created from the body URL using the `hash` function from **ohash**, ensuring consistent hash generation to avoid collisions.\n\nThe URL is stored in the **KV** using the `useStorage` function from **unstorage**, employing the `data` namespace to store the URLs, pre-configured for our use.\n\n\u003CCalloutReadMore text=\"KV Storage\" href=\"https:\u002F\u002Fnitro.unjs.io\u002Fguide\u002Fstorage#usage\" \u002F>\n\nAt the end of this event handler, a route is returned with the shortened URL that users can copy and utilize.\n\nFor development, everything functions correctly, but we must update this configuration, `nitro.config.ts`, for the production environment.\n\n```ts [nitro.config.ts]\nexport default defineNitroConfig({\n  srcDir: 'server',\n  $production: {\n    storage: { data: { driver: 'cloudflare-kv-binding', binding: 'url-shortener' } },\n  },\n})\n```\n\nIn this configuration, we define the `data` namespace, as in development, to use the **cloudflare-kv-binding** driver and the `url-shortener` binding, specified under the key `$production`. This indicates that the configuration applies solely to the **production environment** for accessing the **Cloudflare KV**.\n\n\u003CCalloutReadMore text=\"Environment-specific configuration\" href=\"https:\u002F\u002Fgithub.com\u002Funjs\u002Fc12#environment-specific-configuration\" \u002F>\n\n### Redirecting to the original URL\n\nFinally, we need to establish a route to handle shortened URLs and redirect users to the primary URL. This route is named `[id].get.ts` and is situated in the `server\u002Froutes` directory.\n\n```ts [server\u002Froutes\u002F[id].get.ts]\nexport default defineEventHandler(async (event) => {\n  const id = getRouterParam(event, 'short')\n\n  const value = await useStorage\u003Cstring>('data').getItem(id)\n\n  if (!value) {\n    throw createError({\n      statusCode: 404,\n      statusMessage: 'Not Found',\n    })\n  }\n\n  return sendRedirect(event, value)\n})\n```\n\nHere, we simply obtain the `id` from the router parameters and use `useStorage` to fetch the **URL** from the **KV**. If the URL does not exist, a **404 error** is thrown. Otherwise, the user is redirected to the original URL.\n\n## Deploying the URL shortener\n\nCongratulations! We have successfully created a basic URL shortener using Nitro. The next step is to **deploy** it on Cloudflare Pages. Rest assured, it's straightforward thanks to the **zero-config deployment providers**.\n\n\u003CCalloutReadMore text=\"Zero Config Providers\" href=\"https:\u002F\u002Fnitro.unjs.io\u002Fdeploy#zero-config-providers\" \u002F>\n\n> [!WARNING]\n> A Cloudflare account and a GitHub repository are required to continue with the deployment.\nInitially, log in to the Cloudflare dashboard and select an account. Navigate to Account Home, choose Workers & Pages, and create a new application using the button at the top right. Select the Pages tab and follow the instructions to establish a GitHub connection.\n\nSelect the repository, add the build command `npm run build`, specify the output directory `dist`, include an environment variable `NODE_ENV` with the value `production`, and save and deploy.\n\nNow, let's wait for the deployment to complete. Once done, you can access the URL shortener using the URL provided by **Cloudflare Pages**.\n\n**However**, it may not function adequately initially, as a **KV namespace** has not been bound. This requires manual binding. Go to the project settings, select functions, and scroll until you find **KV namespace bindings**. Add a binding with the name `url-shortener` and choose the desired namespace.\n\n> [!NOTE]\n> Creating a **KV namespace** is achievable in the **Workers & Pages** section.\nRe-deploy your application to account for the new KV namespace binding. Once completed, the URL shortener will be fully functional. :sparkles:\n\n> [!NOTE]\n> If you do not intend to maintain the project, remember to delete it from the Cloudflare Pages dashboard to avoid unnecessary expenses.\n\n## Enhancements and future considerations\n\nTo enhance security, consider adding simple **CSRF protection** using the handler object syntax. Modify the `create.post.tsx` file to include a pre-request handler for origin header verification.\n\n```tsx [server\u002Froutes\u002Fcreate.post.tsx]\nimport { h, Helmet, renderSSR } from 'nano-jsx'\nimport { hash } from 'ohash'\nimport { z } from 'zod'\nimport { withTemplate } from '..\u002Fresources\u002Ftemplate'\n\nexport default defineEventHandler({\n  onBeforeResponse: async (event) => {\n    const requestURL = getRequestURL(event).origin\n    const origin = getRequestHeader(event, 'origin')\n\n    if (!origin) {\n      throw createError({\n        statusCode: 400,\n        statusMessage: 'Bad Request',\n      })\n    }\n\n    if (origin !== requestURL) {\n      throw createError({\n        statusCode: 403,\n        statusMessage: 'Forbidden',\n      })\n    }\n  },\n  handler: async (event) => {\n    const body = await readValidatedBody(event, z.object({\n      url: z.string().url(),\n    }).parse)\n\n    const requestURL = getRequestURL(event)\n    const id = hash(body.url)\n    const shortenURL = new URL(`\u002F${id}`, requestURL).href\n\n    await useStorage('data').setItem(id, body.url)\n\n    const App = () => {\n      return (\n        \u003Cdiv>\n          \u003CHelmet>\n            \u003Ctitle>Created\u003C\u002Ftitle>\n          \u003C\u002FHelmet>\n          \u003Ch2>Created and Ready\u003C\u002Fh2>\n          \u003Cinput\n            type=\"text\"\n            value={shortenURL}\n            autofocus\n          \u002F>\n        \u003C\u002Fdiv>\n      )\n    }\n\n    const app = renderSSR(\u003CApp \u002F>)\n    const { body: nanoBody, head } = Helmet.SSR(app)\n\n    return withTemplate({\n      body: nanoBody,\n      head,\n    })\n  },\n})\n```\n\nWe set up a pre-request handler to check the **origin** header. If there is a mismatch, an error is raised; otherwise, the request proceeds to the main handler.\n\n> [!NOTE]\n> For comprehensive information on **CSRF protection**, refer to the [Cross-Site Request Forgery Prevention Cheat Sheet](https:\u002F\u002Fcheatsheetseries.owasp.org\u002Fcheatsheets\u002FCross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) by OWASP. Validating the **origin** header is a basic defense approach for **CSRF**. However, it is advisable to implement a more robust protection mechanism such as a token.\n\n## Conclusion\n\nDeveloping with **Nitro** and **Cloudflare Pages** is straightforward and provides an exceptional developer experience due to the ability to utilize **TSX** and development storage, eliminating the need for the **Cloudflare Wrangler CLI**.\n\nEnjoy your development journey with **Nitro** and **Cloudflare Pages**! 🚀\n\n> [!NOTE]\n> The URL shortener is inspired by [Yusuke Wada's URL Shortener](https:\u002F\u002Fgithub.com\u002Fyusukebe\u002Furl-shortener).\n",null,{"title":5,"description":3063},{"loc":3066},"posts\u002Fbuild-a-url-shortener-with-nitro-on-cloudflare-pages","CZ53xZ2ywdlK7SVMWbwtizjz4fdUj1L4aFYMJjxKTmU",1790086902048]