[{"data":1,"prerenderedAt":60},["ShallowReactive",2],{"cms-projects-turnstile-forms":3,"cms-projects":24},{"title":4,"excerpt":5,"slug":8,"rawDate":9,"date":10,"modified":11,"content":12,"readingTime":13,"acf":14,"tags":21,"author":22,"featuredMediaUrl":23,"featuredMedia":23},"Turnstile protected static forms",{"raw":6,"plain":7},"\u003Cp>Spam-protected website forms without a form service. A Cloudflare Worker checks Cloudflare Turnstile and emails each submission, and Vue composables handle the form.\u003C\u002Fp>","Spam-protected website forms without a form service. A Cloudflare Worker checks Cloudflare Turnstile and emails each submission, and Vue composables handle the form.","turnstile-forms","2026-10-30T09:00:00",["Date","2026-10-30T00:00:00.000Z"],["Date","2026-11-03T00:00:00.000Z"],"\u003Cp class=\"wp-block-paragraph\">Static sites need contact forms. Netlify offers Netlify Forms. Cloudflare offers email sending and turnstile spam protection, but nothing out of the box for forms end-to-end. I wrote a small package that combines these Cloudflare features. The contact form on this site uses it.\u003C\u002Fp>\n\n\n\n\u003Ch2 class=\"wp-block-heading\">How it works\u003C\u002Fh2>\n\n\n\n\u003Cul class=\"wp-block-list\">\n\u003Cli>Each form is a few lines of config: a title, an email subject, and which fields are allowed and required.\u003C\u002Fli>\n\n\n\n\u003Cli>The Worker only accepts submissions from the site’s own origin, checks the \u003Ca href=\"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fturnstile\u002F\" target=\"_blank\">Turnstile\u003Cpicture>\u003Cimg decoding=\"async\" loading=\"lazy\" width=\"16\" height=\"16\" src=\"\u002Fcdn-cgi\u002Fimage\u002Fformat=auto,quality=80,fit=scale-down,width=32\u002Fimg\u002Ffavicons\u002Fdevelopers.cloudflare.com.png\" alt=\"developers.cloudflare.com favicon\">\u003C\u002Fpicture>\u003C\u002Fa> token, and emails the submission through Cloudflare’s Send Email binding, with Reply-To set to the sender.\u003C\u002Fli>\n\n\n\n\u003Cli>A Turnstile token has to be issued for the site’s hostname with the form’s name as its action, so a token from one form can’t be used to submit another.\u003C\u002Fli>\n\n\n\n\u003Cli>A rejected submission returns an error that names the field to fix, rather than a generic failure.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\n\n\u003Ch2 class=\"wp-block-heading\">The Vue side\u003C\u002Fh2>\n\n\n\n\u003Cul class=\"wp-block-list\">\n\u003Cli>\u003Ccode>useForm\u003C\u002Fcode> saves a draft to localStorage as you type, validates fields, shows a sending state, and resets the Turnstile widget after every attempt, because tokens are single-use.\u003C\u002Fli>\n\n\n\n\u003Cli>\u003Ccode>useTurnstileWidget\u003C\u002Fcode> loads the Turnstile script once per page, however many forms there are.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\n\n\u003Ch2 class=\"wp-block-heading\">Built with\u003C\u002Fh2>\n\n\n\n\u003Cp class=\"wp-block-paragraph\">TypeScript, Cloudflare Workers, Cloudflare Turnstile and Vue 3, with tests, published to GitHub Packages.\u003C\u002Fp>",2,{"project_url":15,"github":16,"icons":17},"","https:\u002F\u002Fgithub.com\u002Fjoebailey26\u002Fturnstile-forms",[18,19,20],"Vue","Cloudflare","TypeScript",[],"Joe",null,[25],[26,41,48],{"title":27,"excerpt":28,"slug":31,"date":32,"modified":33,"readingTime":13,"acf":34,"tags":40,"author":22,"featuredMediaUrl":23,"featuredMedia":23},"Serverless WordPress",{"raw":29,"plain":30},"\u003Cp>WordPress&#8217;s editor without a WordPress server. The editor runs in your browser with WordPress Playground, and Cloudflare stores the content and rebuilds the static site.\u003C\u002Fp>","WordPress’s editor without a WordPress server. The editor runs in your browser with WordPress Playground, and Cloudflare stores the content and rebuilds the static site.","static-wordpress",["Date","2026-11-02T00:00:00.000Z"],["Date","2026-11-03T00:00:00.000Z"],{"project_url":15,"github":35,"icons":36},"https:\u002F\u002Fgithub.com\u002Fjoebailey26\u002Fstatic-wordpress",[37,38,19,39,20],"PHP","WordPress","Nuxt",[],{"title":4,"excerpt":42,"slug":8,"date":43,"modified":44,"readingTime":13,"acf":45,"tags":47,"author":22,"featuredMediaUrl":23,"featuredMedia":23},{"raw":6,"plain":7},["Date","2026-10-30T00:00:00.000Z"],["Date","2026-11-03T00:00:00.000Z"],{"project_url":15,"github":16,"icons":46},[18,19,20],[],{"title":49,"excerpt":50,"slug":53,"date":54,"modified":55,"readingTime":13,"acf":56,"tags":59,"author":22,"featuredMediaUrl":23,"featuredMedia":23},"TV Shows Tracker",{"raw":51,"plain":52},"\u003Cp>A web app that tracks the TV shows I watch, emails me when a show is renewed or cancelled, and adds new episodes to my calendar. I first built it in 2020, then rebuilt it with Nuxt on Cloudflare.\u003C\u002Fp>","A web app that tracks the TV shows I watch, emails me when a show is renewed or cancelled, and adds new episodes to my calendar. I first built it in 2020, then rebuilt it with Nuxt on Cloudflare.","tv-shows",["Date","2020-05-10T00:00:00.000Z"],["Date","2026-11-03T00:00:00.000Z"],{"project_url":15,"github":57,"icons":58},"https:\u002F\u002Fgithub.com\u002Fjoebailey26\u002FTV-Shows",[18,19,39,20],[],1793741743003]