[{"data":1,"prerenderedAt":741},["ShallowReactive",2],{"blog-how-to\u002Fwaitlist-nuxt":3},{"id":4,"title":5,"body":6,"category":711,"date":712,"dateModified":713,"description":714,"draft":715,"extension":716,"faq":717,"featured":715,"keywords":728,"meta":729,"navigation":730,"ogDescription":731,"ogTitle":5,"path":732,"readTime":733,"schemaType":734,"seo":735,"sitemap":736,"stem":737,"tags":738,"twitterCard":739,"__hash__":740},"blog\u002Fblog\u002Fhow-to\u002Fwaitlist-nuxt.md","Nuxt Waitlist: How to Add Signups to a Nuxt App",{"type":7,"value":8,"toc":699},"minimark",[9,38,49,81,86,100,134,150,170,180,188,191,195,202,216,223,228,233,252,261,270,274,277,299,320,329,344,351,357,361,397,400,406,425,428,434,445,449,458,464,487,491,504,522,541,556,562,674],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,37],"p",{},"Nuxt gives you no ",[14,15,16],"code",{},"index.html"," to paste a script tag into. That single missing file is where most Nuxt waitlist setups go wrong: the tag ends up in ",[14,19,20],{},"app.vue",", where Vue inserts it into the DOM but the browser never executes it, and the form silently renders nothing. The other half end up calling ",[14,23,24],{},"window.OperatorStack.joinWaitlist()"," in a ",[14,27,28],{},"\u003Cscript setup>"," body, and ",[14,31,32],{},"nuxt build"," dies with ",[14,35,36],{},"window is not defined"," during prerendering.",[10,39,40,41,44,45,48],{},"Both problems have one-line fixes. This guide covers the correct ",[14,42,43],{},"nuxt.config.ts"," placement, a signup form with the client guard, the hydration timing rule for ",[14,46,47],{},"\u003CClientOnly>"," widgets, and the one kind of Nuxt navigation that page-view tracking misses.",[50,51,52],"tldr",{},[10,53,54,55,57,58,61,62,65,66,69,70,72,73,76,77,80],{},"Register the OperatorStack script in ",[14,56,43],{}," under ",[14,59,60],{},"app.head.script"," with ",[14,63,64],{},"tagPosition: \"bodyClose\"",". Drop a ",[14,67,68],{},"data-os-widget=\"waitlist\""," div on any page for the default form, or call ",[14,71,24],{}," behind an ",[14,74,75],{},"import.meta.client"," guard for a custom one. Normal Nuxt navigation is tracked automatically because Vue Router uses ",[14,78,79],{},"history.pushState",".",[82,83,85],"h2",{"id":84},"step-1-register-the-script-in-nuxtconfigts","Step 1: Register the Script in nuxt.config.ts",[10,87,88,89,92,93,96,97,80],{},"Nuxt builds the document head itself. ",[14,90,91],{},"app.head"," is the app-wide head config, so anything you put there is emitted into every page Nuxt renders, including routes you have flipped to ",[14,94,95],{},"ssr: false"," through ",[14,98,99],{},"routeRules",[101,102,104,124],"step",{"number":103},"1",[10,105,106,119,120,123],{},[107,108,109,110,113,114,116,117,80],"strong",{},"Add a ",[14,111,112],{},"script"," entry to ",[14,115,91],{}," in ",[14,118,43],{}," Replace ",[14,121,122],{},"pk_your_project_key"," with the project key from your OperatorStack dashboard.",[125,126,132],"pre",{"className":127,"code":129,"language":130,"meta":131},[128],"language-ts","\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  app: {\n    head: {\n      script: [\n        {\n          src: \"https:\u002F\u002Foperatorstack.dev\u002Fos.js\",\n          \"data-project\": \"pk_your_project_key\",\n          tagPosition: \"bodyClose\",\n        },\n      ],\n    },\n  },\n});\n","ts","",[14,133,129],{"__ignoreMap":131},[10,135,136,138,139,142,143,146,147,149],{},[14,137,64],{}," puts the tag just before ",[14,140,141],{},"\u003C\u002Fbody>",", which is where you want it: the DOM is parsed by the time it runs, so the embed can find your widget containers on its first pass. This is the same file in Nuxt 3 and Nuxt 4. Nuxt 4 moved your source into ",[14,144,145],{},"app\u002F",", but ",[14,148,43],{}," stays at the project root either way.",[151,152,153],"warning-box",{},[10,154,155,156,159,160,162,163,165,166,169],{},"Do not put ",[14,157,158],{},"\u003Cscript src=\"https:\u002F\u002Foperatorstack.dev\u002Fos.js\">"," in an ",[14,161,20],{}," or layout template. The HTML spec is explicit that a ",[14,164,112],{}," element inserted through ",[14,167,168],{},"innerHTML"," does not execute, and that is the path a Vue template render takes. You get a tag sitting in the DOM inspector that never ran, which is a hard bug to spot because everything looks correct on screen.",[10,171,172,173,176,177,179],{},"If you would rather keep the config in a component, use ",[14,174,175],{},"useHead"," instead of raw markup. It routes through the same head system as ",[14,178,91],{},":",[125,181,186],{"className":182,"code":184,"language":185,"meta":131},[183],"language-vue","\u003C!-- app\u002Fapp.vue -->\n\u003Cscript setup lang=\"ts\">\nuseHead({\n  script: [\n    {\n      src: \"https:\u002F\u002Foperatorstack.dev\u002Fos.js\",\n      \"data-project\": \"pk_your_project_key\",\n      tagPosition: \"bodyClose\",\n    },\n  ],\n});\n\u003C\u002Fscript>\n","vue",[14,187,184],{"__ignoreMap":131},[10,189,190],{},"Pick one. Registering the script in both places loads the embed twice.",[82,192,194],{"id":193},"step-2-drop-in-the-default-widget","Step 2: Drop In the Default Widget",[10,196,197,198,201],{},"For a standard signup form, add the widget div to any page. After the embed initializes it scans the DOM for ",[14,199,200],{},"data-os-widget"," attributes and renders the form inside a Shadow DOM, so your Tailwind or global CSS cannot collide with it and its styles cannot leak into yours.",[101,203,205,210],{"number":204},"2",[10,206,207],{},[107,208,209],{},"Add the widget div to a page component.",[125,211,214],{"className":212,"code":213,"language":185,"meta":131},[183],"\u003C!-- app\u002Fpages\u002Findex.vue -->\n\u003Ctemplate>\n  \u003Csection>\n    \u003Ch2>Join the waitlist\u003C\u002Fh2>\n    \u003Cdiv data-os-widget=\"waitlist\">\u003C\u002Fdiv>\n  \u003C\u002Fsection>\n\u003C\u002Ftemplate>\n",[14,215,213],{"__ignoreMap":131},[10,217,218,219,222],{},"No ",[14,220,221],{},"onMounted",", no template ref, no cleanup. This markup is safe to server-render because it is an empty div until the client-side script fills it.",[224,225,227],"h3",{"id":226},"the-hydration-timing-rule","The Hydration Timing Rule",[10,229,230,231,80],{},"This is the part specific to Nuxt, and it is worth understanding before you wrap anything in ",[14,232,47],{},[10,234,235,236,239,240,243,244,247,248,251],{},"The embed handles the case where your widget container does not exist yet. On startup it checks ",[14,237,238],{},"document.readyState",". If the document is still loading it waits for ",[14,241,242],{},"DOMContentLoaded",". Otherwise it initializes immediately, and if that first pass finds zero widget containers and zero form containers, it attaches a ",[14,245,246],{},"MutationObserver"," to ",[14,249,250],{},"document.body"," and re-initializes as soon as a container appears. That observer disconnects after 10 seconds so it does not leak.",[10,253,254,255,257,258,260],{},"For a server-rendered Nuxt page, none of this matters: your ",[14,256,200],{}," div is in the HTML the server sent, so the first pass finds it. The observer is what covers ",[14,259,47],{}," widgets and SPA-mode routes, where the div appears only after Vue hydrates.",[151,262,263],{},[10,264,265,266,269],{},"The 10-second window is the edge to watch. A widget behind a slow ",[14,267,268],{},"useAsyncData"," call, a lazy component that only loads on scroll, or a modal that opens on click can miss it, and the container will sit empty with no error in the console. If your waitlist form is gated behind something slow, render the container immediately and hide it with CSS rather than mounting it late.",[82,271,273],{"id":272},"step-3-build-a-custom-form-with-script-setup","Step 3: Build a Custom Form with script setup",[10,275,276],{},"When the default widget does not match your design, call the SDK yourself. This is where server rendering starts to matter.",[101,278,280,293],{"number":279},"3",[10,281,282],{},[107,283,284,285,288,289,292],{},"Create a ",[14,286,287],{},"WaitlistForm.vue"," component that guards every ",[14,290,291],{},"window"," access.",[125,294,297],{"className":295,"code":296,"language":185,"meta":131},[183],"\u003C!-- app\u002Fcomponents\u002FWaitlistForm.vue -->\n\u003Cscript setup lang=\"ts\">\nconst email = ref(\"\");\nconst loading = ref(false);\nconst error = ref(\"\");\nconst shareLinks = ref\u003C{ twitter: string; copy: string } | null>(null);\n\nasync function handleSubmit() {\n  if (!import.meta.client) return;\n\n  loading.value = true;\n  error.value = \"\";\n\n  try {\n    await window.OperatorStack.ready;\n    const result = await window.OperatorStack.joinWaitlist({ email: email.value });\n    window.OperatorStack.trackEvent(\"waitlist_signup\", { framework: \"nuxt\" });\n\n    const links = window.OperatorStack.getShareLinks(result.referral_code, {\n      text: \"I just joined the waitlist, check it out:\",\n    });\n    shareLinks.value = { twitter: links.twitter, copy: links.copy };\n  } catch {\n    error.value = \"Something went wrong. Try again.\";\n  } finally {\n    loading.value = false;\n  }\n}\n\nfunction copyLink() {\n  if (shareLinks.value) navigator.clipboard.writeText(shareLinks.value.copy);\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"shareLinks\">\n    \u003Cp>You are on the list. Share to move up.\u003C\u002Fp>\n    \u003Ca :href=\"shareLinks.twitter\">Share on Twitter\u003C\u002Fa>\n    \u003Cbutton @click=\"copyLink\">Copy referral link\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cform v-else @submit.prevent=\"handleSubmit\">\n    \u003Cinput v-model=\"email\" type=\"email\" placeholder=\"your@email.com\" required \u002F>\n    \u003Cbutton type=\"submit\" :disabled=\"loading\">\n      {{ loading ? \"Joining...\" : \"Join waitlist\" }}\n    \u003C\u002Fbutton>\n    \u003Cp v-if=\"error\" role=\"alert\">{{ error }}\u003C\u002Fp>\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n",[14,298,296],{"__ignoreMap":131},[10,300,301,302,305,306,308,309,312,313,315,316,319],{},"The ",[14,303,304],{},"if (!import.meta.client) return"," line is the whole server-render fix. ",[14,307,75],{}," is a compile-time constant that Vite replaces with ",[14,310,311],{},"false"," in the server bundle, so the guard short-circuits and the bundler can drop the ",[14,314,291],{}," code entirely. Use it rather than ",[14,317,318],{},"process.client",", which was the Nuxt 3 spelling and is deprecated.",[321,322,323],"tip-box",{},[10,324,325,328],{},[14,326,327],{},"window.OperatorStack.ready"," is a promise that resolves once the embed has fetched your project config. Await it at the top of the handler instead of assuming the script finished loading before someone clicked submit. Every SDK method awaits it internally too, so a visitor on a slow connection who beats the script to the button gets a queued call rather than a crash.",[10,330,331,332,335,336,339,340,343],{},"You do not have to pass a referral code. If the visitor arrived on a ",[14,333,334],{},"?ref="," link, ",[14,337,338],{},"joinWaitlist()"," reads the stored code and attributes the signup for you. Pass ",[14,341,342],{},"ref"," explicitly only when you are overriding that.",[10,345,346,347,350],{},"For TypeScript, declare the global once in a ",[14,348,349],{},".d.ts"," at the project root. Nuxt includes root-level declaration files in the generated tsconfig, so no import is needed in components:",[125,352,355],{"className":353,"code":354,"language":130,"meta":131},[128],"\u002F\u002F operatorstack.d.ts\ndeclare global {\n  interface Window {\n    OperatorStack: {\n      ready: Promise\u003Cvoid>;\n      joinWaitlist: (params: {\n        email: string;\n        name?: string;\n        ref?: string;\n      }) => Promise\u003C{ success: boolean; referral_code: string; referral_link: string }>;\n      getShareLinks: (\n        code: string,\n        opts?: { text?: string },\n      ) => { twitter: string; linkedin: string; email: string; whatsapp: string; copy: string };\n      trackEvent: (name: string, metadata?: Record\u003Cstring, unknown>) => void;\n      getReferralLink: () => { referral_code: string; referral_link: string } | null;\n      isSignedUp: () => boolean;\n    };\n  }\n}\n\nexport {};\n",[14,356,354],{"__ignoreMap":131},[82,358,360],{"id":359},"route-changes-are-tracked-with-one-exception","Route Changes Are Tracked, With One Exception",[10,362,363,364,366,367,370,371,374,375,378,379,381,382,385,386,389,390,392,393,396],{},"Vue Router calls ",[14,365,79],{}," on every push navigation, whether that came from a ",[14,368,369],{},"\u003CNuxtLink>"," click, a ",[14,372,373],{},"navigateTo()"," call, or ",[14,376,377],{},"router.push()",". The OperatorStack embed patches ",[14,380,79],{}," globally and listens for ",[14,383,384],{},"popstate",", then schedules a page-view event 100ms later. The delay is a debounce: some routers fire both ",[14,387,388],{},"pushState"," and ",[14,391,384],{}," for one navigation, and the timer collapses them into a single event. A ",[14,394,395],{},"lastTrackedUrl"," check drops anything where the URL did not actually change.",[10,398,399],{},"Per-route analytics in a Nuxt app costs you zero tracking calls.",[125,401,404],{"className":402,"code":403,"language":185,"meta":131},[183],"\u003Cscript setup lang=\"ts\">\n\u002F\u002F Already tracked. No router.afterEach hook needed.\nfunction viewPricing() {\n  navigateTo(\"\u002Fpricing\");\n}\n\u003C\u002Fscript>\n",[14,405,403],{"__ignoreMap":131},[151,407,408],{},[10,409,410,411,389,414,417,418,421,422,80],{},"The exception is replace navigation. ",[14,412,413],{},"navigateTo(\"\u002Fthanks\", { replace: true })",[14,415,416],{},"router.replace(\"\u002Fthanks\")"," both call ",[14,419,420],{},"history.replaceState",", which the embed does not patch, so no page view fires. This bites most often on a post-signup redirect, where replacing is the natural choice to keep the form out of the back-button history. If you need that view counted, fire it yourself: ",[14,423,424],{},"window.OperatorStack.trackEvent(\"page_view\", { path: \"\u002Fthanks\" })",[10,426,427],{},"To keep whole sections out of your analytics, such as an authenticated dashboard, exclude them by path prefix on the script entry:",[125,429,432],{"className":430,"code":431,"language":130,"meta":131},[128],"\u002F\u002F nuxt.config.ts\nscript: [\n  {\n    src: \"https:\u002F\u002Foperatorstack.dev\u002Fos.js\",\n    \"data-project\": \"pk_your_project_key\",\n    \"data-exclude\": \"\u002Fadmin,\u002Faccount\",\n    tagPosition: \"bodyClose\",\n  },\n],\n",[14,433,431],{"__ignoreMap":131},[10,435,436,437,440,441,444],{},"The list is comma-separated and matched as a prefix, so ",[14,438,439],{},"\u002Fadmin"," covers ",[14,442,443],{},"\u002Fadmin\u002Fusers"," and everything else beneath it.",[82,446,448],{"id":447},"returning-visitors-and-the-signed-up-state","Returning Visitors and the Signed-Up State",[10,450,451,453,454,457],{},[14,452,338],{}," persists the returned referral code to ",[14,455,456],{},"localStorage"," under a per-project key, so a visitor who already signed up is recognizable on their next visit without you storing anything server-side. That is enough to stop showing a form to someone who already converted.",[125,459,462],{"className":460,"code":461,"language":185,"meta":131},[183],"\u003Cscript setup lang=\"ts\">\nconst alreadyJoined = ref(false);\n\nonMounted(async () => {\n  await window.OperatorStack.ready;\n  alreadyJoined.value = window.OperatorStack.isSignedUp();\n});\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cp v-if=\"alreadyJoined\">You are already on the list. We will email you at launch.\u003C\u002Fp>\n  \u003Cdiv v-else data-os-widget=\"waitlist\">\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[14,463,461],{"__ignoreMap":131},[10,465,466,468,469,471,472,474,475,478,479,482,483,486],{},[14,467,221],{}," is the other valid escape hatch alongside ",[14,470,75],{},". Nuxt never runs it during server rendering, so ",[14,473,291],{}," is guaranteed to exist inside it. If you want the visitor's own referral link back rather than a boolean, ",[14,476,477],{},"getReferralLink()"," returns ",[14,480,481],{},"{ referral_code, referral_link }"," or ",[14,484,485],{},"null"," when they have not signed up.",[82,488,490],{"id":489},"common-mistakes","Common Mistakes",[10,492,493,496,497,116,499,482,501,503],{},[107,494,495],{},"Putting the script tag in a component template."," It renders, it sits in the DOM, and it never runs. ",[14,498,91],{},[14,500,43],{},[14,502,175],{}," in a component are the two paths that work.",[10,505,506,515,516,518,519,521],{},[107,507,508,509,511,512,514],{},"Reaching for ",[14,510,221],{}," when ",[14,513,75],{}," is simpler."," Both work. Use ",[14,517,221],{}," when you need to read state as the component appears, like the signed-up check above. Use ",[14,520,304],{}," inside an event handler, since a submit handler cannot fire on the server anyway and the guard exists to satisfy the build.",[10,523,524,527,528,531,532,116,535,537,538,540],{},[107,525,526],{},"Prerendering a route that calls the SDK at module scope."," ",[14,529,530],{},"nuxt generate"," and any route covered by ",[14,533,534],{},"prerender: true",[14,536,99],{}," execute your setup code at build time. Keep SDK calls inside handlers or ",[14,539,221],{}," and the prerender pass never touches them.",[10,542,543,527,546,548,549,551,552,555],{},[107,544,545],{},"Assuming the widget div needs the SDK.",[14,547,68],{}," needs zero Vue code and zero guards. Reach for ",[14,550,338],{}," only when you want custom UI or need the ",[14,553,554],{},"referral_code"," back to build a share screen.",[10,557,558,561],{},[107,559,560],{},"Mounting the widget container behind a slow fetch."," The embed's re-scan observer gives up after 10 seconds. Render the container up front and toggle its visibility instead of toggling its existence.",[563,564,565,595,621,634,662],"faq-section",{},[566,567,569],"faq-item",{"question":568},"Where does the OperatorStack script tag go in a Nuxt app?",[10,570,571,572,574,575,61,577,580,581,584,585,587,588,591,592,594],{},"In ",[14,573,43],{},", as an entry in ",[14,576,60],{},[14,578,579],{},"tagPosition"," set to ",[14,582,583],{},"bodyClose",". Nuxt has no ",[14,586,16],{}," for you to edit, and a ",[14,589,590],{},"script src"," tag written into a component template does not execute the way the HTML parser runs one. The ",[14,593,91],{}," config is emitted into every page Nuxt renders, so the script loads once and survives client-side navigation.",[566,596,598],{"question":597},"Why do I get window is not defined when I build my Nuxt app?",[10,599,600,601,603,604,607,608,610,611,613,614,617,618,620],{},"Nuxt renders components on the server, where there is no ",[14,602,291],{}," object. A ",[14,605,606],{},"window.OperatorStack"," call in the body of a ",[14,609,28],{}," block runs during the server render and during ",[14,612,32],{}," prerendering. Wrap it in ",[14,615,616],{},"if (import.meta.client)",", or move it into ",[14,619,221],{},", which Nuxt never runs on the server.",[566,622,624],{"question":623},"Do I need a Nuxt module or npm package for OperatorStack?",[10,625,626,627,629,630,633],{},"No. OperatorStack is a single script tag that attaches ",[14,628,606],{}," when it loads. There is no module to add to the ",[14,631,632],{},"modules"," array and no plugin file to write. You call its methods from any component the way you would call any other browser global.",[566,635,637],{"question":636},"Does OperatorStack track page views across Nuxt routes?",[10,638,639,640,642,643,645,646,648,649,61,652,655,656,417,659,661],{},"Yes for normal navigation. Vue Router calls ",[14,641,79],{}," on every push navigation, and the OperatorStack embed patches ",[14,644,388],{}," and listens for ",[14,647,384],{},", then fires a page view 100ms later. The gap is replace navigation: ",[14,650,651],{},"navigateTo",[14,653,654],{},"replace"," set to true and ",[14,657,658],{},"router.replace",[14,660,420],{},", which the embed does not patch, so those are not counted.",[566,663,665],{"question":664},"Does the waitlist widget work inside ClientOnly?",[10,666,667,668,670,671,673],{},"Yes, with one timing caveat. If the embed initializes and finds no widget containers on the page, it watches ",[14,669,250],{}," for containers appearing later and disconnects that observer after 10 seconds. A ",[14,672,47],{}," widget that renders during normal hydration is well inside that window. A widget gated behind a slow fetch that resolves after 10 seconds is not, and will stay empty.",[675,676,677,678],"content-related-articles",{},"\n  ",[679,680,677,684],"contentrelatedcard",{"href":681,"title":682,"description":683},"\u002Fblog\u002Fhow-to\u002Fwaitlist-vue","How to Add a Waitlist to a Vue App","The same script-tag-plus-SDK pattern for a plain Vite Vue project, with no server render step to guard against.",[679,685,677,689],{"href":686,"title":687,"description":688},"\u002Fblog\u002Fhow-to\u002Fnextjs-integration","OperatorStack Next.js Integration Guide","The React-side meta-framework, where the Script component replaces the head config but the hydration rules match.",[679,690,694],{"href":691,"title":692,"description":693},"\u002Fblog\u002Fguides\u002Fjavascript-sdk","OperatorStack JavaScript SDK: All Methods with Examples","Complete reference for joinWaitlist, getShareLinks, trackEvent, and every other SDK method.",[695,696],"cta-box",{"href":697,"label":698},"\u002F","Get Started Free",{"title":131,"searchDepth":700,"depth":700,"links":701},2,[702,703,707,708,709,710],{"id":84,"depth":700,"text":85},{"id":193,"depth":700,"text":194,"children":704},[705],{"id":226,"depth":706,"text":227},3,{"id":272,"depth":700,"text":273},{"id":359,"depth":700,"text":360},{"id":447,"depth":700,"text":448},{"id":489,"depth":700,"text":490},"how-to","2026-08-12",null,"Add an OperatorStack waitlist to Nuxt. Covers the nuxt.config.ts script tag, the import.meta.client guard, and the hydration timing gotcha.",false,"md",[718,720,722,724,726],{"question":568,"answer":719},"In nuxt.config.ts, as an entry in app.head.script with tagPosition set to bodyClose. Nuxt has no index.html for you to edit, and a script src tag written into a component template does not execute the way the HTML parser runs one. The app.head config is emitted into every page Nuxt renders, so the script loads once and survives client-side navigation.",{"question":597,"answer":721},"Nuxt renders components on the server, where there is no window object. A window.OperatorStack call in the body of a script setup block runs during the server render and during nuxt build prerendering. Wrap it in if (import.meta.client), or move it into onMounted, which Nuxt never runs on the server.",{"question":623,"answer":723},"No. OperatorStack is a single script tag that attaches window.OperatorStack when it loads. There is no module to add to the modules array and no plugin file to write. You call its methods from any component the way you would call any other browser global.",{"question":636,"answer":725},"Yes for normal navigation. Vue Router calls history.pushState on every push navigation, and the OperatorStack embed patches pushState and listens for popstate, then fires a page view 100ms later. The gap is replace navigation: navigateTo with replace set to true and router.replace both call history.replaceState, which the embed does not patch, so those are not counted.",{"question":664,"answer":727},"Yes, with one timing caveat. If the embed initializes and finds no widget containers on the page, it watches document.body for containers appearing later and disconnects that observer after 10 seconds. A ClientOnly widget that renders during normal hydration is well inside that window. A widget gated behind a slow fetch that resolves after 10 seconds is not, and will stay empty.","nuxt waitlist,add waitlist to nuxt,operatorstack nuxt,nuxt waitlist form,nuxt window is not defined,nuxt 4 waitlist",{},true,"One script entry in nuxt.config.ts gets you a waitlist, referral links, and route tracking in Nuxt. Mind the client guard and router.replace.","\u002Fblog\u002Fhow-to\u002Fwaitlist-nuxt","7 min","HowTo",{"title":5,"description":714},{"loc":732},"blog\u002Fhow-to\u002Fwaitlist-nuxt",[],"summary_large_image","IzutNI1hUouOpqGib8A3aDAMOPgMuDGLeYt8yVpT6s4",1793197856076]