[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"docs-navigation":3,"docs-page-contact":166},[4,38,57,65,114,138,157],{"id":5,"group":6,"pages":7},"get-started","Get started",[8,13,18,23,28,33],{"path":9,"title":10,"description":11,"navTitle":10,"group":6,"order":12},"\u002Fdocs\u002Fgetting-started","Getting started","Create your app from the kit, run it locally with no credentials, make yourself superadmin and find where your own code goes.",1,{"path":14,"title":15,"description":16,"navTitle":15,"group":6,"order":17},"\u002Fdocs\u002Flayout","Project structure","Where code lives by where it runs, and which server\u002F directories turn a new file into a route, a job or a provider.",2,{"path":19,"title":20,"description":21,"navTitle":20,"group":6,"order":22},"\u002Fdocs\u002Fconfiguration","Configuration","starter.config.ts switches each part of the kit and its providers, environment variables hold the secrets: every key and what it changes.",3,{"path":24,"title":25,"description":26,"navTitle":25,"group":6,"order":27},"\u002Fdocs\u002Ftemplate","Updating from the kit","Keep the kit as a git remote and take each update as one commit you review, with pnpm template:update, then resolve what conflicts.",4,{"path":29,"title":30,"description":31,"navTitle":30,"group":6,"order":32},"\u002Fdocs\u002Fdeployment","Deployment","Ship the app as one Cloudflare Worker: create the resources, set the secrets, deploy main from GitHub Actions or your machine.",5,{"path":34,"title":35,"description":36,"navTitle":35,"group":6,"order":37},"\u002Fdocs\u002Fmcp","MCP server","Connect Claude Code, Cursor or VS Code to https:\u002F\u002Fnuxter.app\u002Fmcp, so your agent searches and reads these docs and their design notes.",6,{"id":39,"group":40,"pages":41},"accounts","Accounts",[42,47,52],{"path":43,"title":44,"description":45,"navTitle":44,"group":40,"order":46},"\u002Fdocs\u002Fauth","Auth","Pick the sign-in methods, add an OAuth provider, protect a route, and handle sessions, second factors and account deletion.",7,{"path":48,"title":49,"description":50,"navTitle":49,"group":40,"order":51},"\u002Fdocs\u002Ftenancy","Organizations","Scope routes and pages to an organization, and work with its members, roles, seats, invitations and a second level such as branches.",8,{"path":53,"title":54,"description":55,"navTitle":54,"group":40,"order":56},"\u002Fdocs\u002Fpermissions","Roles and permissions","Routes check permissions that an account’s role or its organization’s plan grants: checking one, adding a role, and the staff tools.",9,{"id":58,"group":59,"pages":60},"payments","Payments",[61],{"path":62,"title":59,"description":63,"navTitle":59,"group":59,"order":64},"\u002Fdocs\u002Fpayments","Sell plans through Stripe, Polar, Lemon Squeezy or Creem: define plans, connect a provider, gate features, trials, coupons and refunds.",10,{"id":66,"group":67,"pages":68},"building-blocks","Building blocks",[69,74,79,84,89,94,99,104,109],{"path":70,"title":71,"description":72,"navTitle":71,"group":67,"order":73},"\u002Fdocs\u002Fdatabase","Database","Postgres through Drizzle, on Hyperdrive in production and PGlite locally: add a table, query, transactions, migrations and seed data.",11,{"path":75,"title":76,"description":77,"navTitle":76,"group":67,"order":78},"\u002Fdocs\u002Fbackground-jobs","Background jobs","Run work after the response on Cloudflare Queues with retries: add and enqueue a job, run jobs locally, and schedule cron tasks.",12,{"path":80,"title":81,"description":82,"navTitle":81,"group":67,"order":83},"\u002Fdocs\u002Fevents","Events","React to what the kit does by adding a file to server\u002Flisteners\u002F, fire events of your own, and look up every event’s payload.",13,{"path":85,"title":86,"description":87,"navTitle":86,"group":67,"order":88},"\u002Fdocs\u002Femail","Email","Set up sending, send your own mail with deliverEmail, change the templates and wording, handle unsubscribes and add a provider.",14,{"path":90,"title":91,"description":92,"navTitle":91,"group":67,"order":93},"\u002Fdocs\u002Fcontact","Contact","A \u002Fcontact page that mails you a message or shows your address after a Turnstile check: choose the channels, set the address, know the limits.",15,{"path":95,"title":96,"description":97,"navTitle":96,"group":67,"order":98},"\u002Fdocs\u002Fnotifications","Notifications","The dashboard bell: add a notification type, send one with notify(), and how categories, opt-outs and retention apply.",16,{"path":100,"title":101,"description":102,"navTitle":101,"group":67,"order":103},"\u002Fdocs\u002Fstorage","Storage","Uploads in R2 with one policy per purpose: add a purpose, accept an upload, replace or delete a file, and serve it to its owner.",17,{"path":105,"title":106,"description":107,"navTitle":106,"group":67,"order":108},"\u002Fdocs\u002Ffeature-flags","Feature flags","Declare typed flags in starter.config.ts, read them with flag() or useFlag(), roll them out by rule or percentage, and add a provider.",18,{"path":110,"title":111,"description":112,"navTitle":111,"group":67,"order":113},"\u002Fdocs\u002Fai","AI","Call models from server code by role: text, images, speech, transcription, embeddings and video, with provider fallbacks.",19,{"id":115,"group":116,"pages":117},"web","Web",[118,123,128,133],{"path":119,"title":120,"description":121,"navTitle":120,"group":116,"order":122},"\u002Fdocs\u002Fsecurity-headers","Security headers","The Content-Security-Policy and hardening headers on every response: allow another origin, keep tokens out of Referer, accept cross-site posts.",20,{"path":124,"title":125,"description":126,"navTitle":125,"group":116,"order":127},"\u002Fdocs\u002Frate-limiting","Rate limiting","Cap how often a caller hits a route with requireRateLimit, count only failures, space out sends, and protect the zone.",21,{"path":129,"title":130,"description":131,"navTitle":130,"group":116,"order":132},"\u002Fdocs\u002Fanalytics","Analytics","Product analytics and ad conversions sent from the server with no browser script: add an event, connect a provider, and handle consent.",22,{"path":134,"title":135,"description":136,"navTitle":135,"group":116,"order":137},"\u002Fdocs\u002Fobservability","Observability","Find a failed request by its id, send faults to Sentry or Better Stack, report an error you caught, and change the error page.",23,{"id":139,"group":140,"pages":141},"content-and-look","Content and look",[142,147,152],{"path":143,"title":144,"description":145,"navTitle":144,"group":140,"order":146},"\u002Fdocs\u002Fblog","Blog","Markdown posts with an RSS feed per language, and workflows that draft posts with an AI model and open each one as a pull request.",24,{"path":148,"title":149,"description":150,"navTitle":149,"group":140,"order":151},"\u002Fdocs\u002Flanguages","Languages","Add or remove a language, write copy in the locale files, and write code, refusals and links that follow the reader’s language.",25,{"path":153,"title":154,"description":155,"navTitle":154,"group":140,"order":156},"\u002Fdocs\u002Ftheme","Theme","Change the brand colour and font, and build dashboard sections, list pages, loading placeholders and charts that match the kit.",26,{"id":158,"group":159,"pages":160},"quality","Quality",[161],{"path":162,"title":163,"description":164,"navTitle":163,"group":159,"order":165},"\u002Fdocs\u002Ftesting","Testing","Unit tests in Node, component tests in a real Nuxt app and Playwright end to end: test a route or a component, and what CI runs.",27,{"page":167,"surround":666,"linksToKit":669},{"path":90,"title":91,"description":92,"navTitle":91,"group":67,"seo":168,"body":170},{"title":169,"description":92},"Nuxt contact form with Turnstile",{"type":171,"value":172,"toc":657},"minimark",[173,177,185,190,274,278,338,390,406,410,417,460,473,484,488,505,509,562,586,590,596,600,610,633,653],[174,175,91],"h1",{"id":176},"contact",[178,179,180,184],"p",{},[181,182,183],"code",{},"\u002Fcontact"," lets visitors reach you without your address appearing in any page.\nIt offers a form that mails you their message, a \"Show email address\" button\nthat hands out the address after a Turnstile check, or both. You work here when\nyou set the address for a deploy, choose what the page offers or change how it\nlooks.",[186,187,189],"h2",{"id":188},"how-it-works","How it works",[191,192,193,204,236,251],"ol",{},[194,195,196,199,200,203],"li",{},[181,197,198],{},"NUXT_CONTACT_ADDRESS"," fills ",[181,201,202],{},"contactAddress",", a private runtime-config key.\nOnly the two routes below read it, at request time, so no build, page,\npayload or bundle carries it.",[194,205,206,216,217,220,221,224,225,228,229,224,232,235],{},[207,208,213],"a",{"href":209,"rel":210,"title":212},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fmodules\u002Fcontact-routes.ts",[211],"nofollow","Licensees only",[181,214,215],{},"modules\u002Fcontact-routes.ts"," registers one\nroute per entry in ",[181,218,219],{},"contact.channels",": ",[181,222,223],{},"POST \u002Fapi\u002Fcontact"," for ",[181,226,227],{},"form"," and\n",[181,230,231],{},"POST \u002Fapi\u002Fcontact\u002Faddress",[181,233,234],{},"reveal",". A channel left out answers 404.",[194,237,238,239,242,243,246,247,250],{},"Each route counts the attempt against the caller's IP, then checks that the\naddress is set and that Turnstile passes. The form sends the message through\n",[181,240,241],{},"deliverEmail"," with the sender as Reply-To, once, while the request lasts:\nit is never queued or stored. The reveal answers ",[181,244,245],{},"{ address }"," with\n",[181,248,249],{},"cache-control: no-store",".",[194,252,253,254,257,258,261,262,265,266,269,270,273],{},"The kit's support links (",[181,255,256],{},"SupportLink",", the footers and ",[181,259,260],{},":contact-line"," in\nthe legal pages) take their path from ",[181,263,264],{},"useContactPath()",", which is ",[181,267,268],{},"null","\nwhile ",[181,271,272],{},"contact.enabled"," is false, so they render nothing then.",[186,275,277],{"id":276},"choose-the-channels","Choose the channels",[279,280,285],"pre",{"className":281,"code":282,"language":283,"meta":284,"style":284},"language-ts shiki shiki-themes github-light github-light github-dark","contact: {\n  enabled: true,\n  channels: ['form', 'reveal'],\n},\n","ts","",[181,286,287,298,312,333],{"__ignoreMap":284},[288,289,291,294],"span",{"class":290,"line":12},"line",[288,292,176],{"class":293},"snPdu",[288,295,297],{"class":296},"sxrX7",": {\n",[288,299,300,303,305,309],{"class":290,"line":17},[288,301,302],{"class":293},"  enabled",[288,304,220],{"class":296},[288,306,308],{"class":307},"sBjJW","true",[288,310,311],{"class":296},",\n",[288,313,314,317,320,324,327,330],{"class":290,"line":22},[288,315,316],{"class":293},"  channels",[288,318,319],{"class":296},": [",[288,321,323],{"class":322},"sIIMD","'form'",[288,325,326],{"class":296},", ",[288,328,329],{"class":322},"'reveal'",[288,331,332],{"class":296},"],\n",[288,334,335],{"class":290,"line":27},[288,336,337],{"class":296},"},\n",[339,340,341,356],"table",{},[342,343,344],"thead",{},[345,346,347,353],"tr",{},[348,349,350],"th",{},[181,351,352],{},"channels",[348,354,355],{},"The page shows",[357,358,359,370,380],"tbody",{},[345,360,361,367],{},[362,363,364],"td",{},[181,365,366],{},"['form', 'reveal']",[362,368,369],{},"The form, with \"Prefer to write directly? Show email address\" below",[345,371,372,377],{},[362,373,374],{},[181,375,376],{},"['form']",[362,378,379],{},"The form alone",[345,381,382,387],{},[362,383,384],{},[181,385,386],{},"['reveal']",[362,388,389],{},"\"Show email address\" as the page's only button",[178,391,392,395,396,398,399,401,402,405],{},[181,393,394],{},"enabled: false"," drops the page and both routes from the build. The build\nrefuses an empty ",[181,397,352],{},", a channel it doesn't know, and ",[181,400,227],{}," while\n",[181,403,404],{},"email.enabled"," is false.",[186,407,409],{"id":408},"set-the-address","Set the address",[178,411,412,413,416],{},"Locally, put it in ",[181,414,415],{},".env",". On a deploy, set it on the Worker:",[279,418,422],{"className":419,"code":420,"language":421,"meta":284,"style":284},"language-bash shiki shiki-themes github-light github-light github-dark","pnpm exec wrangler secret put NUXT_CONTACT_ADDRESS --name \u003Cworker>\n","bash",[181,423,424],{"__ignoreMap":284},[288,425,426,429,432,435,438,441,444,447,451,454,457],{"class":290,"line":12},[288,427,428],{"class":293},"pnpm",[288,430,431],{"class":322}," exec",[288,433,434],{"class":322}," wrangler",[288,436,437],{"class":322}," secret",[288,439,440],{"class":322}," put",[288,442,443],{"class":322}," NUXT_CONTACT_ADDRESS",[288,445,446],{"class":307}," --name",[288,448,450],{"class":449},"s8jYJ"," \u003C",[288,452,453],{"class":322},"worke",[288,455,456],{"class":296},"r",[288,458,459],{"class":449},">\n",[178,461,462,463,466,467,469,470,472],{},"Until it is set, both routes answer 503. The ",[181,464,465],{},"[env]"," line a deployed Worker\nlogs at boot names everything the page still lacks: the address, the Turnstile\nkeys and, with ",[181,468,227],{},", a configured email provider (",[207,471,86],{"href":85},").",[178,474,475,476,479,480,483],{},"Keep ",[181,477,478],{},"NUXT_EMAIL_FROM"," and ",[181,481,482],{},"NUXT_EMAIL_REPLY_TO"," on other addresses. Every mail\nthe app sends carries both, and anyone can ask for a sign-in mail to their own\ninbox. The boot check warns when either one is the contact address.",[186,485,487],{"id":486},"turnstile","Turnstile",[178,489,490,491,494,495,497,498,501,502,504],{},"Both routes call ",[181,492,493],{},"assertHuman"," (",[207,496,44],{"href":43},") before they send or answer.\nOutside ",[181,499,500],{},"pnpm dev",", the contact routes answer 503 while the Turnstile keys are\nunset, where the auth forms would skip the check. ",[181,503,500],{}," runs on\nCloudflare's always-pass test keys.",[186,506,508],{"id":507},"limits","Limits",[339,510,511,521],{},[342,512,513],{},[345,514,515,518],{},[348,516,517],{},"What",[348,519,520],{},"Limit",[357,522,523,531,539,547,554],{},[345,524,525,528],{},[362,526,527],{},"Attempts per IP address",[362,529,530],{},"5 in 10 minutes on each route, then locked for 10 minutes. IPv6 by \u002F64.",[345,532,533,536],{},[362,534,535],{},"Messages, site-wide",[362,537,538],{},"30 an hour, counted once Turnstile passes",[345,540,541,544],{},[362,542,543],{},"Name",[362,545,546],{},"1 to 100 characters, on one line",[345,548,549,551],{},[362,550,86],{},[362,552,553],{},"Up to 254 characters",[345,555,556,559],{},[362,557,558],{},"Message",[362,560,561],{},"10 to 5,000 characters, with no control characters but tabs and newlines",[178,563,564,565,568,569,571,572,228,579,250],{},"Over a limit the route answers 429 with ",[181,566,567],{},"Retry-After","\n(",[207,570,125],{"href":124},"). A message every provider refuses\nanswers 502 and is not retried; the form keeps it so the visitor can send it\nagain. The numbers live in\n",[207,573,576],{"href":574,"rel":575,"title":212},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Fcontact\u002Flimits.ts",[211],[181,577,578],{},"server\u002Fcontact\u002Flimits.ts",[207,580,583],{"href":581,"rel":582,"title":212},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fshared\u002Fschemas\u002Fcontact.ts",[211],[181,584,585],{},"shared\u002Fschemas\u002Fcontact.ts",[186,587,589],{"id":588},"what-you-receive","What you receive",[178,591,592,593,595],{},"A mail in plain text from ",[181,594,478],{}," with the subject \"Contact from\" and\nthe sender's name, and the sender's address as Reply-To. Its first line names\nthe signed-in account that sent it, marked when that account hasn't proven its\naddress yet, or says it was sent signed out. The name and Reply-To are marked\nunverified, because anyone can type any address: confirm a request about an\naccount with that account's address before you act on it.",[186,597,599],{"id":598},"change-the-page","Change the page",[178,601,602,609],{},[207,603,606],{"href":604,"rel":605,"title":212},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fapp\u002Fpages\u002Fcontact.vue",[211],[181,607,608],{},"app\u002Fpages\u002Fcontact.vue"," is a short page around two\ncomponents, so a page of your own can use them the same way:",[611,612,613,623],"ul",{},[194,614,615,618,619,622],{},[181,616,617],{},"\u003CContactForm @sent=\"...\">"," is the form. It fills in a signed-in account's\nname and address, and emits ",[181,620,621],{},"sent"," with the reply address once the message\nis accepted.",[194,624,625,628,629,632],{},[181,626,627],{},"\u003CContactReveal>"," is the reveal. ",[181,630,631],{},"standalone"," draws it as a full-width button,\nfor a page with no form.",[178,634,635,636,639,640,643,644,647,648,650,651,250],{},"Render each one only when its channel is in ",[181,637,638],{},"starter.contact.channels",", since\nthe route behind a channel that is off answers 404. The copy is under\n",[181,641,642],{},"contact.*"," in the locale files. To link to the page from your own code, use\n",[181,645,646],{},"\u003CSupportLink>"," or ",[181,649,264],{},", both of which follow ",[181,652,272],{},[654,655,656],"style",{},"html pre.shiki code .snPdu, html code.shiki .snPdu{--shiki-light:#6F42C1;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sxrX7, html code.shiki .sxrX7{--shiki-light:#24292E;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sBjJW, html code.shiki .sBjJW{--shiki-light:#005CC5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sIIMD, html code.shiki .sIIMD{--shiki-light:#032F62;--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .s8jYJ, html code.shiki .s8jYJ{--shiki-light:#D73A49;--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":284,"searchDepth":17,"depth":17,"links":658},[659,660,661,662,663,664,665],{"id":188,"depth":17,"text":189},{"id":276,"depth":17,"text":277},{"id":408,"depth":17,"text":409},{"id":486,"depth":17,"text":487},{"id":507,"depth":17,"text":508},{"id":588,"depth":17,"text":589},{"id":598,"depth":17,"text":599},[667,668],{"path":85,"title":86,"description":87,"navTitle":86,"group":67,"order":88},{"path":95,"title":96,"description":97,"navTitle":96,"group":67,"order":98},true]