[{"data":1,"prerenderedAt":393},["ShallowReactive",2],{"blog-how-to\u002Fwaitlist-vue":3},{"id":4,"title":5,"body":6,"category":367,"date":368,"dateModified":369,"description":370,"draft":371,"extension":372,"faq":373,"featured":371,"keywords":380,"meta":381,"navigation":382,"ogDescription":383,"ogTitle":5,"path":384,"readTime":385,"schemaType":386,"seo":387,"sitemap":388,"stem":389,"tags":390,"twitterCard":391,"__hash__":392},"blog\u002Fblog\u002Fhow-to\u002Fwaitlist-vue.md","How to Add a Waitlist to a Vue App",{"type":7,"value":8,"toc":359},"minimark",[9,25,54,59,65,98,102,105,132,136,145,182,195,199,216,222,251,255,264,284,300,334],[10,11,12,13,17,18,20,21,24],"p",{},"A Vite-scaffolded Vue app has one ",[14,15,16],"code",{},"index.html"," that never touches a server render step, so there is no hydration mismatch to guard against and no framework-specific script-loading prop to configure. The script tag goes in ",[14,19,16],{}," like it would on a plain HTML page, and ",[14,22,23],{},"window.OperatorStack"," is available the moment your app mounts. This guide covers the setup, a Composition API form, and what Vue Router does automatically once the script loads.",[26,27,28],"tldr",{},[10,29,30,31,33,34,37,38,41,42,45,46,49,50,53],{},"Add the OperatorStack script tag to ",[14,32,16],{}," before ",[14,35,36],{},"\u003C\u002Fbody>",". Use the ",[14,39,40],{},"data-os-widget=\"waitlist\""," div for the default form, or call ",[14,43,44],{},"window.OperatorStack.joinWaitlist()"," from a ",[14,47,48],{},"ref()","-backed component for a custom UI. Vue Router's history-mode navigation is tracked automatically since OperatorStack patches ",[14,51,52],{},"history.pushState",".",[55,56,58],"h2",{"id":57},"step-1-add-the-script-tag-to-indexhtml","Step 1: Add the Script Tag to index.html",[10,60,61,62,64],{},"A Vite Vue app (and a Vue CLI app before it) serves one static ",[14,63,16],{}," that never re-renders server-side. Add the script there once.",[66,67,69,85,95],"step",{"number":68},"1",[10,70,71,80,81,84],{},[72,73,74,75,77,78,53],"strong",{},"Open ",[14,76,16],{}," and add the script before ",[14,79,36],{}," Replace ",[14,82,83],{},"pk_your_project_key"," with your project key from the OperatorStack dashboard.",[86,87,93],"pre",{"className":88,"code":90,"language":91,"meta":92},[89],"language-html","\u003C!-- index.html -->\n\u003C!doctype html>\n\u003Chtml lang=\"en\">\n  \u003Chead>\n    \u003Cmeta charset=\"UTF-8\" \u002F>\n    \u003Ctitle>Your App\u003C\u002Ftitle>\n  \u003C\u002Fhead>\n  \u003Cbody>\n    \u003Cdiv id=\"app\">\u003C\u002Fdiv>\n    \u003Cscript type=\"module\" src=\"\u002Fsrc\u002Fmain.ts\">\u003C\u002Fscript>\n    \u003Cscript src=\"https:\u002F\u002Foperatorstack.dev\u002Fos.js\" data-project=\"pk_your_project_key\">\u003C\u002Fscript>\n  \u003C\u002Fbody>\n\u003C\u002Fhtml>\n","html","",[14,94,90],{"__ignoreMap":92},[10,96,97],{},"There is no build-time transform and no Vue plugin to register. The browser executes the script the same way it would on any static page, after your Vue bundle has already started mounting.",[55,99,101],{"id":100},"step-2-use-the-default-widget-div","Step 2: Use the Default Widget Div",[10,103,104],{},"For a standard signup form with no custom styling logic, drop the widget div directly in a template. OperatorStack scans for it once the script loads and renders the form inside a Shadow DOM, so it never collides with your app's CSS.",[66,106,108,113,121],{"number":107},"2",[10,109,110],{},[72,111,112],{},"Render the widget div in any single-file component.",[86,114,119],{"className":115,"code":117,"language":118,"meta":92},[116],"language-vue","\u003C!-- src\u002Fcomponents\u002FWaitlistSection.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","vue",[14,120,117],{"__ignoreMap":92},[10,122,123,124,127,128,131],{},"No ",[14,125,126],{},"onMounted",", no template ref, no cleanup logic. The script tag from Step 1 scans the DOM for ",[14,129,130],{},"data-os-widget"," attributes once it initializes, regardless of which component the div lives in.",[55,133,135],{"id":134},"step-3-build-a-custom-form-with-the-composition-api","Step 3: Build a Custom Form with the Composition API",[10,137,138,139,141,142,53],{},"If the default widget does not match your design system, call the SDK directly from a component built around ",[14,140,48],{}," and ",[14,143,144],{},"reactive()",[66,146,148,161,167],{"number":147},"3",[10,149,150],{},[72,151,152,153,156,157,160],{},"Create a ",[14,154,155],{},"WaitlistForm.vue"," component that calls ",[14,158,159],{},"joinWaitlist"," on submit.",[86,162,165],{"className":163,"code":164,"language":118,"meta":92},[116],"\u003C!-- src\u002Fcomponents\u002FWaitlistForm.vue -->\n\u003Cscript setup lang=\"ts\">\nimport { ref } from \"vue\";\n\ndeclare global {\n  interface Window {\n    OperatorStack: {\n      ready: Promise\u003Cvoid>;\n      joinWaitlist: (params: {\n        email: string;\n        name?: 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; copy: string; email: string; whatsapp: string };\n      trackEvent: (name: string, data?: Record\u003Cstring, unknown>) => void;\n    };\n  }\n}\n\nconst email = ref(\"\");\nconst loading = ref(false);\nconst shareLinks = ref\u003C{ twitter: string; copy: string } | null>(null);\n\nasync function handleSubmit() {\n  loading.value = true;\n\n  await window.OperatorStack.ready;\n  const result = await window.OperatorStack.joinWaitlist({ email: email.value });\n  window.OperatorStack.trackEvent(\"waitlist_signup\", { source: document.referrer || \"direct\" });\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  loading.value = false;\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=\"navigator.clipboard.writeText(shareLinks.copy)\">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  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n",[14,166,164],{"__ignoreMap":92},[10,168,169,170,173,174,177,178,181],{},"The ",[14,171,172],{},"Window"," interface declaration is optional but removes the ",[14,175,176],{},"Property 'OperatorStack' does not exist on type 'Window'"," TypeScript error everywhere you reference it. Put it in one ",[14,179,180],{},"shims-operatorstack.d.ts"," file at the project root instead of repeating it per component.",[183,184,185],"tip-box",{},[10,186,187,190,191,194],{},[14,188,189],{},"window.OperatorStack.ready"," is a promise that resolves once the SDK has fetched your project config. Await it at the top of ",[14,192,193],{},"handleSubmit"," (as above) rather than assuming the script has finished loading by the time someone clicks submit.",[55,196,198],{"id":197},"route-changes-are-tracked-automatically-in-history-mode","Route Changes Are Tracked Automatically, in History Mode",[10,200,201,202,205,206,208,209,211,212,215],{},"Vue Router's default ",[14,203,204],{},"createWebHistory()"," calls ",[14,207,52],{}," on every navigation, the same mechanism a full page load would trigger via the browser's address bar. OperatorStack's embed script patches ",[14,210,52],{}," globally and listens for ",[14,213,214],{},"popstate",", then fires a page-view event 100ms after either one, debounced to avoid double-counting when a navigation triggers both. That gives a single-page Vue app per-route analytics without wiring up a router-specific tracking call.",[86,217,220],{"className":218,"code":219,"language":118,"meta":92},[116],"\u003Cscript setup lang=\"ts\">\nimport { useRouter } from \"vue-router\";\n\n\u002F\u002F No extra code needed -- this navigation is already tracked\nconst router = useRouter();\nfunction goToPricing() {\n  router.push(\"\u002Fpricing\");\n}\n\u003C\u002Fscript>\n",[14,221,219],{"__ignoreMap":92},[223,224,225],"warning-box",{},[10,226,227,228,230,231,234,235,238,239,242,243,246,247,250],{},"This only works with ",[14,229,204],{},". If your router uses ",[14,232,233],{},"createWebHashHistory()"," (URLs like ",[14,236,237],{},"\u002F#\u002Fpricing","), route changes update the hash instead of calling ",[14,240,241],{},"pushState",", so OperatorStack will not see them and you will only get the initial page view. Switch to history mode, or fire ",[14,244,245],{},"window.OperatorStack.trackEvent(\"page_view\", { path: to.path })"," manually from a ",[14,248,249],{},"router.afterEach"," guard if hash mode is a hard requirement.",[55,252,254],{"id":253},"common-mistakes","Common Mistakes",[10,256,257,260,261,263],{},[72,258,259],{},"Putting the script tag inside a component instead of index.html."," A script tag rendered in a Vue template only executes once, the first time that component mounts, and Vue's reactivity can skip re-inserting it on subsequent renders. Keep it in ",[14,262,16],{}," where it loads exactly once, guaranteed.",[10,265,266,269,270,272,273,276,277,280,281,53],{},[72,267,268],{},"Forgetting the TypeScript global declaration."," Without it, every ",[14,271,23],{}," reference is a type error in ",[14,274,275],{},"\u003Cscript setup lang=\"ts\">"," blocks. Add it once in a ",[14,278,279],{},".d.ts"," file included by your ",[14,282,283],{},"tsconfig.json",[10,285,286,289,290,292,293,295,296,299],{},[72,287,288],{},"Assuming you need a composable for the default widget."," The ",[14,291,40],{}," div works with zero Vue-specific code. Reach for the SDK and ",[14,294,48],{}," only when you need a custom UI or want to read back ",[14,297,298],{},"referral_code"," for a share screen.",[301,302,303,310,316,322,328],"faq-section",{},[304,305,307],"faq-item",{"question":306},"Where does the OperatorStack script tag go in a Vue app?",[10,308,309],{},"In index.html, right before the closing  tag. A plain client-side Vue app built with Vite has no server render step, so window is already available by the time your root component mounts. No onMounted guard is needed just to reference window.OperatorStack from a script tag in the same document.",[304,311,313],{"question":312},"Do I need an npm package to use OperatorStack in Vue?",[10,314,315],{},"No. OperatorStack is a script tag, not an npm package. It attaches window.OperatorStack once it loads, and you call its methods from your components exactly like any other browser global, including from the Composition API.",[304,317,319],{"question":318},"Does OperatorStack track page views across Vue Router routes?",[10,320,321],{},"Yes, automatically, in history mode. Vue Router's createWebHistory calls history.pushState on every navigation. OperatorStack patches pushState and listens for popstate, then fires a page-view event 100ms later, debounced so a route change never counts twice. Hash mode routers do not update pushState the same way, so if you use createWebHashHistory you will only see the initial page view.",[304,323,325],{"question":324},"Can I use TypeScript with the OperatorStack SDK in Vue?",[10,326,327],{},"Yes. Add a global Window interface declaration once in a shims-operatorstack.d.ts file so window.OperatorStack has types across every component and composable, or cast to any if you only reference it in one place.",[304,329,331],{"question":330},"How do I avoid a flash of empty space before the waitlist widget renders?",[10,332,333],{},"Await window.OperatorStack.ready before showing the form, or render a skeleton until it resolves. The ready promise resolves once the SDK has fetched your project config, which typically takes under a second.",[335,336,337,338],"content-related-articles",{},"\n  ",[339,340,337,344],"contentrelatedcard",{"href":341,"title":342,"description":343},"\u002Fblog\u002Fguides\u002Fjavascript-sdk","OperatorStack JavaScript SDK: All Methods with Examples","Complete reference for joinWaitlist, getShareLinks, trackEvent, and all other SDK methods with runnable examples.",[339,345,337,349],{"href":346,"title":347,"description":348},"\u002Fblog\u002Fhow-to\u002Fwaitlist-react","How to Add a Waitlist to a React App","The same script-tag-plus-SDK pattern for a Vite or Create React App project instead of Vue.",[339,350,354],{"href":351,"title":352,"description":353},"\u002Fblog\u002Fhow-to\u002Fwaitlist-nuxt","Nuxt Waitlist: How to Add Signups to a Nuxt App","The same Vue components on Nuxt, plus the nuxt.config.ts script placement and the client guard server rendering needs.",[355,356],"cta-box",{"href":357,"label":358},"\u002F","Get Started Free",{"title":92,"searchDepth":360,"depth":360,"links":361},2,[362,363,364,365,366],{"id":57,"depth":360,"text":58},{"id":100,"depth":360,"text":101},{"id":134,"depth":360,"text":135},{"id":197,"depth":360,"text":198},{"id":253,"depth":360,"text":254},"how-to","2026-07-24","2026-09-02","Add a waitlist to a Vite or Vue CLI app. Covers the index.html script tag, a Composition API form with ref(), and Vue Router page tracking.",false,"md",[374,376,377,378,379],{"question":306,"answer":375},"In index.html, right before the closing \u003C\u002Fbody> tag. A plain client-side Vue app built with Vite has no server render step, so window is already available by the time your root component mounts. No onMounted guard is needed just to reference window.OperatorStack from a script tag in the same document.",{"question":312,"answer":315},{"question":318,"answer":321},{"question":324,"answer":327},{"question":330,"answer":333},"vue waitlist,add waitlist to vue app,operatorstack vue,vite vue waitlist,vue composition api waitlist,vue spa analytics",{},true,"One script tag in index.html plus a Composition API form gets you a waitlist, referral links, and page-view tracking in a Vue SPA. No SSR to worry about.","\u002Fblog\u002Fhow-to\u002Fwaitlist-vue","6 min","HowTo",{"title":5,"description":370},{"loc":384},"blog\u002Fhow-to\u002Fwaitlist-vue",[],"summary_large_image","1h8BYnfLOzhetUKfCK_KUhBGUDdYFI-o3dsqGFTe92s",1793197856076]