[{"data":1,"prerenderedAt":1145},["ShallowReactive",2],{"blog-deploying-nuxt-to-cloudflare-workers":3,"blog-post-nav":1107},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"body":11,"_type":1101,"_id":1102,"_source":1103,"_file":1104,"_stem":1105,"_extension":1106},"/blog/deploying-nuxt-to-cloudflare-workers","blog",false,"","Deploying this Nuxt site to Cloudflare Workers","How this site moved from Netlify to Cloudflare Workers, and the runtime-config fight that took five commits to win.","2026-06-06",{"type":12,"children":13,"toc":1094},"root",[14,31,38,59,131,144,239,260,266,290,542,547,553,589,602,636,648,661,666,956,969,975,980,1049,1062,1068,1088],{"type":15,"tag":16,"props":17,"children":18},"element","p",{},[19,22,29],{"type":20,"value":21},"text","This very website is a Nuxt 3 app: Nuxt Content for the blog you're reading, Nuxt Image for assets, ",{"type":15,"tag":23,"props":24,"children":26},"code",{"className":25},[],[27],{"type":20,"value":28},"@vueuse/motion",{"type":20,"value":30}," for the page transitions, and JetBrains Mono pulled in at build time. For a long time it lived on Netlify. This post is about moving it to Cloudflare Workers, and specifically about the one part that fought me the hardest: getting a secret to the server at runtime.",{"type":15,"tag":32,"props":33,"children":35},"h2",{"id":34},"from-netlify-to-a-worker",[36],{"type":20,"value":37},"From Netlify to a Worker",{"type":15,"tag":16,"props":39,"children":40},{},[41,43,49,51,57],{"type":20,"value":42},"The original deploy was the boring, happy path: push to a repo, Netlify builds it, Netlify Forms handles the contact form. Migrating to Cloudflare meant picking a Nitro preset, and that took a few false starts. I tried ",{"type":15,"tag":23,"props":44,"children":46},{"className":45},[],[47],{"type":20,"value":48},"cloudflare-pages",{"type":20,"value":50},", reverted, fiddled with ",{"type":15,"tag":23,"props":52,"children":54},{"className":53},[],[55],{"type":20,"value":56},"pages_build_output_dir",{"type":20,"value":58},", and eventually landed on the one that actually fits a full Nuxt app with server routes:",{"type":15,"tag":60,"props":61,"children":65},"pre",{"className":62,"code":63,"language":64,"meta":7,"style":7},"language-ts shiki shiki-themes github-dark","// nuxt.config.ts\nnitro: {\n  preset: 'cloudflare-module',\n},\n","ts",[66],{"type":15,"tag":23,"props":67,"children":68},{"__ignoreMap":7},[69,81,97,122],{"type":15,"tag":70,"props":71,"children":74},"span",{"class":72,"line":73},"line",1,[75],{"type":15,"tag":70,"props":76,"children":78},{"style":77},"--shiki-default:#6A737D",[79],{"type":20,"value":80},"// nuxt.config.ts\n",{"type":15,"tag":70,"props":82,"children":84},{"class":72,"line":83},2,[85,91],{"type":15,"tag":70,"props":86,"children":88},{"style":87},"--shiki-default:#B392F0",[89],{"type":20,"value":90},"nitro",{"type":15,"tag":70,"props":92,"children":94},{"style":93},"--shiki-default:#E1E4E8",[95],{"type":20,"value":96},": {\n",{"type":15,"tag":70,"props":98,"children":100},{"class":72,"line":99},3,[101,106,111,117],{"type":15,"tag":70,"props":102,"children":103},{"style":87},[104],{"type":20,"value":105},"  preset",{"type":15,"tag":70,"props":107,"children":108},{"style":93},[109],{"type":20,"value":110},": ",{"type":15,"tag":70,"props":112,"children":114},{"style":113},"--shiki-default:#9ECBFF",[115],{"type":20,"value":116},"'cloudflare-module'",{"type":15,"tag":70,"props":118,"children":119},{"style":93},[120],{"type":20,"value":121},",\n",{"type":15,"tag":70,"props":123,"children":125},{"class":72,"line":124},4,[126],{"type":15,"tag":70,"props":127,"children":128},{"style":93},[129],{"type":20,"value":130},"},\n",{"type":15,"tag":16,"props":132,"children":133},{},[134,136,142],{"type":20,"value":135},"That preset builds a single Worker entry point. The ",{"type":15,"tag":23,"props":137,"children":139},{"className":138},[],[140],{"type":20,"value":141},"wrangler.toml",{"type":20,"value":143}," points at it and serves the built static files through an assets binding:",{"type":15,"tag":60,"props":145,"children":149},{"className":146,"code":147,"language":148,"meta":7,"style":7},"language-toml shiki shiki-themes github-dark","name = \"alfiemills-website\"\nmain = \".output/server/index.mjs\"\ncompatibility_date = \"2024-11-01\"\ncompatibility_flags = [\"nodejs_compat\", \"nodejs_compat_populate_process_env\"]\n\nworkers_dev = false\n\n[assets]\ndirectory = \".output/public\"\nbinding = \"ASSETS\"\n","toml",[150],{"type":15,"tag":23,"props":151,"children":152},{"__ignoreMap":7},[153,161,169,177,185,195,204,212,221,230],{"type":15,"tag":70,"props":154,"children":155},{"class":72,"line":73},[156],{"type":15,"tag":70,"props":157,"children":158},{},[159],{"type":20,"value":160},"name = \"alfiemills-website\"\n",{"type":15,"tag":70,"props":162,"children":163},{"class":72,"line":83},[164],{"type":15,"tag":70,"props":165,"children":166},{},[167],{"type":20,"value":168},"main = \".output/server/index.mjs\"\n",{"type":15,"tag":70,"props":170,"children":171},{"class":72,"line":99},[172],{"type":15,"tag":70,"props":173,"children":174},{},[175],{"type":20,"value":176},"compatibility_date = \"2024-11-01\"\n",{"type":15,"tag":70,"props":178,"children":179},{"class":72,"line":124},[180],{"type":15,"tag":70,"props":181,"children":182},{},[183],{"type":20,"value":184},"compatibility_flags = [\"nodejs_compat\", \"nodejs_compat_populate_process_env\"]\n",{"type":15,"tag":70,"props":186,"children":188},{"class":72,"line":187},5,[189],{"type":15,"tag":70,"props":190,"children":192},{"emptyLinePlaceholder":191},true,[193],{"type":20,"value":194},"\n",{"type":15,"tag":70,"props":196,"children":198},{"class":72,"line":197},6,[199],{"type":15,"tag":70,"props":200,"children":201},{},[202],{"type":20,"value":203},"workers_dev = false\n",{"type":15,"tag":70,"props":205,"children":207},{"class":72,"line":206},7,[208],{"type":15,"tag":70,"props":209,"children":210},{"emptyLinePlaceholder":191},[211],{"type":20,"value":194},{"type":15,"tag":70,"props":213,"children":215},{"class":72,"line":214},8,[216],{"type":15,"tag":70,"props":217,"children":218},{},[219],{"type":20,"value":220},"[assets]\n",{"type":15,"tag":70,"props":222,"children":224},{"class":72,"line":223},9,[225],{"type":15,"tag":70,"props":226,"children":227},{},[228],{"type":20,"value":229},"directory = \".output/public\"\n",{"type":15,"tag":70,"props":231,"children":233},{"class":72,"line":232},10,[234],{"type":15,"tag":70,"props":235,"children":236},{},[237],{"type":20,"value":238},"binding = \"ASSETS\"\n",{"type":15,"tag":16,"props":240,"children":241},{},[242,244,250,252,258],{"type":20,"value":243},"One non-obvious line there is ",{"type":15,"tag":23,"props":245,"children":247},{"className":246},[],[248],{"type":20,"value":249},"workers_dev = false",{"type":20,"value":251},". The site is served on a custom domain, not a ",{"type":15,"tag":23,"props":253,"children":255},{"className":254},[],[256],{"type":20,"value":257},"*.workers.dev",{"type":20,"value":259}," subdomain, and leaving the subdomain enabled makes wrangler fire a post-deploy API call to provision it. That call intermittently 503s and fails the whole deploy for no good reason. Turning it off skips the call entirely.",{"type":15,"tag":32,"props":261,"children":263},{"id":262},"the-contact-form-take-two",[264],{"type":20,"value":265},"The contact form, take two",{"type":15,"tag":16,"props":267,"children":268},{},[269,271,280,282,288],{"type":20,"value":270},"Netlify Forms obviously don't exist on Cloudflare, so the contact form became a Nitro server route that posts to ",{"type":15,"tag":272,"props":273,"children":277},"a",{"href":274,"rel":275},"https://resend.com",[276],"nofollow",[278],{"type":20,"value":279},"Resend",{"type":20,"value":281},". It's small: a honeypot field to swat bots, basic validation, then a ",{"type":15,"tag":23,"props":283,"children":285},{"className":284},[],[286],{"type":20,"value":287},"fetch",{"type":20,"value":289}," to the Resend API.",{"type":15,"tag":60,"props":291,"children":293},{"className":62,"code":292,"language":64,"meta":7,"style":7},"// Honeypot\nif (body['bot-field']) {\n  return { ok: true }\n}\n\nconst { name, email, message } = body\nif (!name?.trim() || !email?.trim() || !message?.trim()) {\n  throw createError({ statusCode: 400, message: 'All fields are required' })\n}\n",[294],{"type":15,"tag":23,"props":295,"children":296},{"__ignoreMap":7},[297,305,329,353,361,368,420,497,535],{"type":15,"tag":70,"props":298,"children":299},{"class":72,"line":73},[300],{"type":15,"tag":70,"props":301,"children":302},{"style":77},[303],{"type":20,"value":304},"// Honeypot\n",{"type":15,"tag":70,"props":306,"children":307},{"class":72,"line":83},[308,314,319,324],{"type":15,"tag":70,"props":309,"children":311},{"style":310},"--shiki-default:#F97583",[312],{"type":20,"value":313},"if",{"type":15,"tag":70,"props":315,"children":316},{"style":93},[317],{"type":20,"value":318}," (body[",{"type":15,"tag":70,"props":320,"children":321},{"style":113},[322],{"type":20,"value":323},"'bot-field'",{"type":15,"tag":70,"props":325,"children":326},{"style":93},[327],{"type":20,"value":328},"]) {\n",{"type":15,"tag":70,"props":330,"children":331},{"class":72,"line":99},[332,337,342,348],{"type":15,"tag":70,"props":333,"children":334},{"style":310},[335],{"type":20,"value":336},"  return",{"type":15,"tag":70,"props":338,"children":339},{"style":93},[340],{"type":20,"value":341}," { ok: ",{"type":15,"tag":70,"props":343,"children":345},{"style":344},"--shiki-default:#79B8FF",[346],{"type":20,"value":347},"true",{"type":15,"tag":70,"props":349,"children":350},{"style":93},[351],{"type":20,"value":352}," }\n",{"type":15,"tag":70,"props":354,"children":355},{"class":72,"line":124},[356],{"type":15,"tag":70,"props":357,"children":358},{"style":93},[359],{"type":20,"value":360},"}\n",{"type":15,"tag":70,"props":362,"children":363},{"class":72,"line":187},[364],{"type":15,"tag":70,"props":365,"children":366},{"emptyLinePlaceholder":191},[367],{"type":20,"value":194},{"type":15,"tag":70,"props":369,"children":370},{"class":72,"line":197},[371,376,381,386,391,396,400,405,410,415],{"type":15,"tag":70,"props":372,"children":373},{"style":310},[374],{"type":20,"value":375},"const",{"type":15,"tag":70,"props":377,"children":378},{"style":93},[379],{"type":20,"value":380}," { ",{"type":15,"tag":70,"props":382,"children":383},{"style":344},[384],{"type":20,"value":385},"name",{"type":15,"tag":70,"props":387,"children":388},{"style":93},[389],{"type":20,"value":390},", ",{"type":15,"tag":70,"props":392,"children":393},{"style":344},[394],{"type":20,"value":395},"email",{"type":15,"tag":70,"props":397,"children":398},{"style":93},[399],{"type":20,"value":390},{"type":15,"tag":70,"props":401,"children":402},{"style":344},[403],{"type":20,"value":404},"message",{"type":15,"tag":70,"props":406,"children":407},{"style":93},[408],{"type":20,"value":409}," } ",{"type":15,"tag":70,"props":411,"children":412},{"style":310},[413],{"type":20,"value":414},"=",{"type":15,"tag":70,"props":416,"children":417},{"style":93},[418],{"type":20,"value":419}," body\n",{"type":15,"tag":70,"props":421,"children":422},{"class":72,"line":206},[423,427,432,437,442,447,452,457,462,467,471,475,479,483,488,492],{"type":15,"tag":70,"props":424,"children":425},{"style":310},[426],{"type":20,"value":313},{"type":15,"tag":70,"props":428,"children":429},{"style":93},[430],{"type":20,"value":431}," (",{"type":15,"tag":70,"props":433,"children":434},{"style":310},[435],{"type":20,"value":436},"!",{"type":15,"tag":70,"props":438,"children":439},{"style":93},[440],{"type":20,"value":441},"name?.",{"type":15,"tag":70,"props":443,"children":444},{"style":87},[445],{"type":20,"value":446},"trim",{"type":15,"tag":70,"props":448,"children":449},{"style":93},[450],{"type":20,"value":451},"() ",{"type":15,"tag":70,"props":453,"children":454},{"style":310},[455],{"type":20,"value":456},"||",{"type":15,"tag":70,"props":458,"children":459},{"style":310},[460],{"type":20,"value":461}," !",{"type":15,"tag":70,"props":463,"children":464},{"style":93},[465],{"type":20,"value":466},"email?.",{"type":15,"tag":70,"props":468,"children":469},{"style":87},[470],{"type":20,"value":446},{"type":15,"tag":70,"props":472,"children":473},{"style":93},[474],{"type":20,"value":451},{"type":15,"tag":70,"props":476,"children":477},{"style":310},[478],{"type":20,"value":456},{"type":15,"tag":70,"props":480,"children":481},{"style":310},[482],{"type":20,"value":461},{"type":15,"tag":70,"props":484,"children":485},{"style":93},[486],{"type":20,"value":487},"message?.",{"type":15,"tag":70,"props":489,"children":490},{"style":87},[491],{"type":20,"value":446},{"type":15,"tag":70,"props":493,"children":494},{"style":93},[495],{"type":20,"value":496},"()) {\n",{"type":15,"tag":70,"props":498,"children":499},{"class":72,"line":214},[500,505,510,515,520,525,530],{"type":15,"tag":70,"props":501,"children":502},{"style":310},[503],{"type":20,"value":504},"  throw",{"type":15,"tag":70,"props":506,"children":507},{"style":87},[508],{"type":20,"value":509}," createError",{"type":15,"tag":70,"props":511,"children":512},{"style":93},[513],{"type":20,"value":514},"({ statusCode: ",{"type":15,"tag":70,"props":516,"children":517},{"style":344},[518],{"type":20,"value":519},"400",{"type":15,"tag":70,"props":521,"children":522},{"style":93},[523],{"type":20,"value":524},", message: ",{"type":15,"tag":70,"props":526,"children":527},{"style":113},[528],{"type":20,"value":529},"'All fields are required'",{"type":15,"tag":70,"props":531,"children":532},{"style":93},[533],{"type":20,"value":534}," })\n",{"type":15,"tag":70,"props":536,"children":537},{"class":72,"line":223},[538],{"type":15,"tag":70,"props":539,"children":540},{"style":93},[541],{"type":20,"value":360},{"type":15,"tag":16,"props":543,"children":544},{},[545],{"type":20,"value":546},"Simple enough. Except it kept failing in production with a 500, because the Resend API key was never reaching the handler.",{"type":15,"tag":32,"props":548,"children":550},{"id":549},"the-runtime-config-fight",[551],{"type":20,"value":552},"The runtime-config fight",{"type":15,"tag":16,"props":554,"children":555},{},[556,558,564,566,572,574,580,582,587],{"type":20,"value":557},"In Nuxt, secrets live in ",{"type":15,"tag":23,"props":559,"children":561},{"className":560},[],[562],{"type":20,"value":563},"runtimeConfig",{"type":20,"value":565}," and you read them with ",{"type":15,"tag":23,"props":567,"children":569},{"className":568},[],[570],{"type":20,"value":571},"useRuntimeConfig()",{"type":20,"value":573},". Locally, Nuxt populates that from a ",{"type":15,"tag":23,"props":575,"children":577},{"className":576},[],[578],{"type":20,"value":579},"NUXT_RESEND_API_KEY",{"type":20,"value":581}," environment variable and everything works. On Cloudflare Workers, it just wasn't there. The key was set as a Worker secret, but ",{"type":15,"tag":23,"props":583,"children":585},{"className":584},[],[586],{"type":20,"value":571},{"type":20,"value":588}," came back empty.",{"type":15,"tag":16,"props":590,"children":591},{},[592,594,600],{"type":20,"value":593},"The reason is that Workers don't expose secrets on ",{"type":15,"tag":23,"props":595,"children":597},{"className":596},[],[598],{"type":20,"value":599},"process.env",{"type":20,"value":601}," the way Node does. Nitro hands you the request's environment through the H3 event instead, and Nuxt's auto-imported helper doesn't reach for it unless you give it the event. So the first fix was passing the event in:",{"type":15,"tag":60,"props":603,"children":605},{"className":62,"code":604,"language":64,"meta":7,"style":7},"const config = useRuntimeConfig(event)\n",[606],{"type":15,"tag":23,"props":607,"children":608},{"__ignoreMap":7},[609],{"type":15,"tag":70,"props":610,"children":611},{"class":72,"line":73},[612,616,621,626,631],{"type":15,"tag":70,"props":613,"children":614},{"style":310},[615],{"type":20,"value":375},{"type":15,"tag":70,"props":617,"children":618},{"style":344},[619],{"type":20,"value":620}," config",{"type":15,"tag":70,"props":622,"children":623},{"style":310},[624],{"type":20,"value":625}," =",{"type":15,"tag":70,"props":627,"children":628},{"style":87},[629],{"type":20,"value":630}," useRuntimeConfig",{"type":15,"tag":70,"props":632,"children":633},{"style":93},[634],{"type":20,"value":635},"(event)\n",{"type":15,"tag":16,"props":637,"children":638},{},[639,641,646],{"type":20,"value":640},"That helped, but not everywhere, so the next move was telling the Workers runtime to actually populate ",{"type":15,"tag":23,"props":642,"children":644},{"className":643},[],[645],{"type":20,"value":599},{"type":20,"value":647}," from bindings, via a compatibility flag:",{"type":15,"tag":60,"props":649,"children":650},{"className":146,"code":184,"language":148,"meta":7,"style":7},[651],{"type":15,"tag":23,"props":652,"children":653},{"__ignoreMap":7},[654],{"type":15,"tag":70,"props":655,"children":656},{"class":72,"line":73},[657],{"type":15,"tag":70,"props":658,"children":659},{},[660],{"type":20,"value":184},{"type":15,"tag":16,"props":662,"children":663},{},[664],{"type":20,"value":665},"Still flaky across environments. The honest truth is I stopped trying to find the one correct source and instead read from all of them, in order, falling through until something is non-empty:",{"type":15,"tag":60,"props":667,"children":669},{"className":62,"code":668,"language":64,"meta":7,"style":7},"const config = useRuntimeConfig(event)\nconst globalEnv = (globalThis as any).__env__ as Record\u003Cstring, string | undefined> | undefined\nconst cfEnv = (event.context.cloudflare as any)?.env as Record\u003Cstring, string | undefined> | undefined\n\nconst resendApiKey =\n  config.resendApiKey ||\n  globalEnv?.NUXT_RESEND_API_KEY ||\n  cfEnv?.NUXT_RESEND_API_KEY ||\n  process.env.NUXT_RESEND_API_KEY ||\n  ''\n",[670],{"type":15,"tag":23,"props":671,"children":672},{"__ignoreMap":7},[673,696,784,862,869,886,899,916,932,948],{"type":15,"tag":70,"props":674,"children":675},{"class":72,"line":73},[676,680,684,688,692],{"type":15,"tag":70,"props":677,"children":678},{"style":310},[679],{"type":20,"value":375},{"type":15,"tag":70,"props":681,"children":682},{"style":344},[683],{"type":20,"value":620},{"type":15,"tag":70,"props":685,"children":686},{"style":310},[687],{"type":20,"value":625},{"type":15,"tag":70,"props":689,"children":690},{"style":87},[691],{"type":20,"value":630},{"type":15,"tag":70,"props":693,"children":694},{"style":93},[695],{"type":20,"value":635},{"type":15,"tag":70,"props":697,"children":698},{"class":72,"line":83},[699,703,708,712,717,722,727,732,736,741,746,751,755,759,764,769,774,779],{"type":15,"tag":70,"props":700,"children":701},{"style":310},[702],{"type":20,"value":375},{"type":15,"tag":70,"props":704,"children":705},{"style":344},[706],{"type":20,"value":707}," globalEnv",{"type":15,"tag":70,"props":709,"children":710},{"style":310},[711],{"type":20,"value":625},{"type":15,"tag":70,"props":713,"children":714},{"style":93},[715],{"type":20,"value":716}," (globalThis ",{"type":15,"tag":70,"props":718,"children":719},{"style":310},[720],{"type":20,"value":721},"as",{"type":15,"tag":70,"props":723,"children":724},{"style":344},[725],{"type":20,"value":726}," any",{"type":15,"tag":70,"props":728,"children":729},{"style":93},[730],{"type":20,"value":731},").__env__ ",{"type":15,"tag":70,"props":733,"children":734},{"style":310},[735],{"type":20,"value":721},{"type":15,"tag":70,"props":737,"children":738},{"style":87},[739],{"type":20,"value":740}," Record",{"type":15,"tag":70,"props":742,"children":743},{"style":93},[744],{"type":20,"value":745},"\u003C",{"type":15,"tag":70,"props":747,"children":748},{"style":344},[749],{"type":20,"value":750},"string",{"type":15,"tag":70,"props":752,"children":753},{"style":93},[754],{"type":20,"value":390},{"type":15,"tag":70,"props":756,"children":757},{"style":344},[758],{"type":20,"value":750},{"type":15,"tag":70,"props":760,"children":761},{"style":310},[762],{"type":20,"value":763}," |",{"type":15,"tag":70,"props":765,"children":766},{"style":344},[767],{"type":20,"value":768}," undefined",{"type":15,"tag":70,"props":770,"children":771},{"style":93},[772],{"type":20,"value":773},"> ",{"type":15,"tag":70,"props":775,"children":776},{"style":310},[777],{"type":20,"value":778},"|",{"type":15,"tag":70,"props":780,"children":781},{"style":344},[782],{"type":20,"value":783}," undefined\n",{"type":15,"tag":70,"props":785,"children":786},{"class":72,"line":99},[787,791,796,800,805,809,813,818,822,826,830,834,838,842,846,850,854,858],{"type":15,"tag":70,"props":788,"children":789},{"style":310},[790],{"type":20,"value":375},{"type":15,"tag":70,"props":792,"children":793},{"style":344},[794],{"type":20,"value":795}," cfEnv",{"type":15,"tag":70,"props":797,"children":798},{"style":310},[799],{"type":20,"value":625},{"type":15,"tag":70,"props":801,"children":802},{"style":93},[803],{"type":20,"value":804}," (event.context.cloudflare ",{"type":15,"tag":70,"props":806,"children":807},{"style":310},[808],{"type":20,"value":721},{"type":15,"tag":70,"props":810,"children":811},{"style":344},[812],{"type":20,"value":726},{"type":15,"tag":70,"props":814,"children":815},{"style":93},[816],{"type":20,"value":817},")?.env ",{"type":15,"tag":70,"props":819,"children":820},{"style":310},[821],{"type":20,"value":721},{"type":15,"tag":70,"props":823,"children":824},{"style":87},[825],{"type":20,"value":740},{"type":15,"tag":70,"props":827,"children":828},{"style":93},[829],{"type":20,"value":745},{"type":15,"tag":70,"props":831,"children":832},{"style":344},[833],{"type":20,"value":750},{"type":15,"tag":70,"props":835,"children":836},{"style":93},[837],{"type":20,"value":390},{"type":15,"tag":70,"props":839,"children":840},{"style":344},[841],{"type":20,"value":750},{"type":15,"tag":70,"props":843,"children":844},{"style":310},[845],{"type":20,"value":763},{"type":15,"tag":70,"props":847,"children":848},{"style":344},[849],{"type":20,"value":768},{"type":15,"tag":70,"props":851,"children":852},{"style":93},[853],{"type":20,"value":773},{"type":15,"tag":70,"props":855,"children":856},{"style":310},[857],{"type":20,"value":778},{"type":15,"tag":70,"props":859,"children":860},{"style":344},[861],{"type":20,"value":783},{"type":15,"tag":70,"props":863,"children":864},{"class":72,"line":124},[865],{"type":15,"tag":70,"props":866,"children":867},{"emptyLinePlaceholder":191},[868],{"type":20,"value":194},{"type":15,"tag":70,"props":870,"children":871},{"class":72,"line":187},[872,876,881],{"type":15,"tag":70,"props":873,"children":874},{"style":310},[875],{"type":20,"value":375},{"type":15,"tag":70,"props":877,"children":878},{"style":344},[879],{"type":20,"value":880}," resendApiKey",{"type":15,"tag":70,"props":882,"children":883},{"style":310},[884],{"type":20,"value":885}," =\n",{"type":15,"tag":70,"props":887,"children":888},{"class":72,"line":197},[889,894],{"type":15,"tag":70,"props":890,"children":891},{"style":93},[892],{"type":20,"value":893},"  config.resendApiKey ",{"type":15,"tag":70,"props":895,"children":896},{"style":310},[897],{"type":20,"value":898},"||\n",{"type":15,"tag":70,"props":900,"children":901},{"class":72,"line":206},[902,907,911],{"type":15,"tag":70,"props":903,"children":904},{"style":93},[905],{"type":20,"value":906},"  globalEnv?.",{"type":15,"tag":70,"props":908,"children":909},{"style":344},[910],{"type":20,"value":579},{"type":15,"tag":70,"props":912,"children":913},{"style":310},[914],{"type":20,"value":915}," ||\n",{"type":15,"tag":70,"props":917,"children":918},{"class":72,"line":214},[919,924,928],{"type":15,"tag":70,"props":920,"children":921},{"style":93},[922],{"type":20,"value":923},"  cfEnv?.",{"type":15,"tag":70,"props":925,"children":926},{"style":344},[927],{"type":20,"value":579},{"type":15,"tag":70,"props":929,"children":930},{"style":310},[931],{"type":20,"value":915},{"type":15,"tag":70,"props":933,"children":934},{"class":72,"line":223},[935,940,944],{"type":15,"tag":70,"props":936,"children":937},{"style":93},[938],{"type":20,"value":939},"  process.env.",{"type":15,"tag":70,"props":941,"children":942},{"style":344},[943],{"type":20,"value":579},{"type":15,"tag":70,"props":945,"children":946},{"style":310},[947],{"type":20,"value":915},{"type":15,"tag":70,"props":949,"children":950},{"class":72,"line":232},[951],{"type":15,"tag":70,"props":952,"children":953},{"style":113},[954],{"type":20,"value":955},"  ''\n",{"type":15,"tag":16,"props":957,"children":958},{},[959,961,967],{"type":20,"value":960},"It's belt, braces, and a second belt. Not elegant, but ",{"type":15,"tag":23,"props":962,"children":964},{"className":963},[],[965],{"type":20,"value":966},"event.context.cloudflare.env",{"type":20,"value":968}," is the one that reliably has the value on Workers, and the fallback chain means the same code path also works in local dev and in a plain Node preview without me thinking about it. That whole saga was five commits I'd rather not relive, all to move one string from a secret store into a request handler.",{"type":15,"tag":32,"props":970,"children":972},{"id":971},"seeing-what-the-worker-is-doing",[973],{"type":20,"value":974},"Seeing what the Worker is doing",{"type":15,"tag":16,"props":976,"children":977},{},[978],{"type":20,"value":979},"The last piece was turning on observability so I'm not debugging blind. Workers can persist logs and invocation traces, which is the difference between \"the form is broken\" and \"Resend returned a 422 because of the from address\":",{"type":15,"tag":60,"props":981,"children":983},{"className":146,"code":982,"language":148,"meta":7,"style":7},"[observability]\nenabled = true\nhead_sampling_rate = 1\n\n[observability.logs]\nenabled = true\npersist = true\ninvocation_logs = true\n",[984],{"type":15,"tag":23,"props":985,"children":986},{"__ignoreMap":7},[987,995,1003,1011,1018,1026,1033,1041],{"type":15,"tag":70,"props":988,"children":989},{"class":72,"line":73},[990],{"type":15,"tag":70,"props":991,"children":992},{},[993],{"type":20,"value":994},"[observability]\n",{"type":15,"tag":70,"props":996,"children":997},{"class":72,"line":83},[998],{"type":15,"tag":70,"props":999,"children":1000},{},[1001],{"type":20,"value":1002},"enabled = true\n",{"type":15,"tag":70,"props":1004,"children":1005},{"class":72,"line":99},[1006],{"type":15,"tag":70,"props":1007,"children":1008},{},[1009],{"type":20,"value":1010},"head_sampling_rate = 1\n",{"type":15,"tag":70,"props":1012,"children":1013},{"class":72,"line":124},[1014],{"type":15,"tag":70,"props":1015,"children":1016},{"emptyLinePlaceholder":191},[1017],{"type":20,"value":194},{"type":15,"tag":70,"props":1019,"children":1020},{"class":72,"line":187},[1021],{"type":15,"tag":70,"props":1022,"children":1023},{},[1024],{"type":20,"value":1025},"[observability.logs]\n",{"type":15,"tag":70,"props":1027,"children":1028},{"class":72,"line":197},[1029],{"type":15,"tag":70,"props":1030,"children":1031},{},[1032],{"type":20,"value":1002},{"type":15,"tag":70,"props":1034,"children":1035},{"class":72,"line":206},[1036],{"type":15,"tag":70,"props":1037,"children":1038},{},[1039],{"type":20,"value":1040},"persist = true\n",{"type":15,"tag":70,"props":1042,"children":1043},{"class":72,"line":214},[1044],{"type":15,"tag":70,"props":1045,"children":1046},{},[1047],{"type":20,"value":1048},"invocation_logs = true\n",{"type":15,"tag":16,"props":1050,"children":1051},{},[1052,1054,1060],{"type":20,"value":1053},"With that on, the ",{"type":15,"tag":23,"props":1055,"children":1057},{"className":1056},[],[1058],{"type":20,"value":1059},"console.error('Resend error:', ...)",{"type":20,"value":1061}," in the catch path actually shows up somewhere I can read it.",{"type":15,"tag":32,"props":1063,"children":1065},{"id":1064},"what-id-tell-past-me",[1066],{"type":20,"value":1067},"What I'd tell past-me",{"type":15,"tag":16,"props":1069,"children":1070},{},[1071,1073,1078,1080,1086],{"type":20,"value":1072},"Two things. First, on Cloudflare, reach for ",{"type":15,"tag":23,"props":1074,"children":1076},{"className":1075},[],[1077],{"type":20,"value":966},{"type":20,"value":1079}," directly for secrets and stop fighting ",{"type":15,"tag":23,"props":1081,"children":1083},{"className":1082},[],[1084],{"type":20,"value":1085},"useRuntimeConfig",{"type":20,"value":1087},"; the abstraction leaks here. Second, turn on observability before you need it, not after the third failed deploy. The migration itself was straightforward once the preset was right. It was the invisible runtime environment, the thing you can't see until it's wrong, that ate the time.\n\n",{"type":15,"tag":1089,"props":1090,"children":1091},"style",{},[1092],{"type":20,"value":1093},"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);}",{"title":7,"searchDepth":83,"depth":83,"links":1095},[1096,1097,1098,1099,1100],{"id":34,"depth":83,"text":37},{"id":262,"depth":83,"text":265},{"id":549,"depth":83,"text":552},{"id":971,"depth":83,"text":974},{"id":1064,"depth":83,"text":1067},"markdown","content:blog:deploying-nuxt-to-cloudflare-workers.md","content","blog/deploying-nuxt-to-cloudflare-workers.md","blog/deploying-nuxt-to-cloudflare-workers","md",[1108,1109,1113,1117,1121,1125,1129,1133,1137,1141],{"_path":4,"title":8,"date":10},{"_path":1110,"title":1111,"date":1112},"/blog/building-forever-llm","Building forever-llm: three takes on a model that never stops","2026-05-20",{"_path":1114,"title":1115,"date":1116},"/blog/laravel-cortex-adhd-productivity","Building Cortex: An ADHD Productivity App in Laravel + Inertia","2026-05-12",{"_path":1118,"title":1119,"date":1120},"/blog/eink-spotify-weather-clock","Building an E-Ink Clock That Shows Spotify, Weather and the Time","2026-05-05",{"_path":1122,"title":1123,"date":1124},"/blog/hetzner-k8s-cluster","Building a K3s Cluster on Hetzner with Terraform and GitOps","2026-04-20",{"_path":1126,"title":1127,"date":1128},"/blog/arduino-uno-q-forza-rev-gauge","A Forza Rev Gauge on the Arduino UNO Q's LED Matrix","2026-04-15",{"_path":1130,"title":1131,"date":1132},"/blog/modular-go-echo-gorm","A Modular Go Web App Pattern with Echo, GORM and golang-migrate","2026-04-05",{"_path":1134,"title":1135,"date":1136},"/blog/self-hosted-ios-web-push","Self-hosting iOS push notifications with Web Push and PWAs","2026-03-25",{"_path":1138,"title":1139,"date":1140},"/blog/lit-web-components-astro-docs","Building a framework-free web component library with Lit and Astro","2026-03-10",{"_path":1142,"title":1143,"date":1144},"/blog/welcome-to-my-blog","About this site","2024-01-15",1781294950569]