<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Matt Ferderer</title>
    <description>The latest articles on DEV Community by Matt Ferderer (@mattferderer).</description>
    <link>https://dev.to/mattferderer</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F72593%2F35a810e6-7427-4cce-8111-d625682e7994.jpg</url>
      <title>DEV Community: Matt Ferderer</title>
      <link>https://dev.to/mattferderer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mattferderer"/>
    <language>en</language>
    <item>
      <title>Submit a Static Website Form with Cloudflare Workers</title>
      <dc:creator>Matt Ferderer</dc:creator>
      <pubDate>Fri, 11 Nov 2022 23:01:16 +0000</pubDate>
      <link>https://dev.to/mattferderer/submit-a-static-website-form-with-cloudflare-workers-27hk</link>
      <guid>https://dev.to/mattferderer/submit-a-static-website-form-with-cloudflare-workers-27hk</guid>
      <description>&lt;p&gt;I had a recent small project for a family member's business I took on that I did with a static website. The website needed a contact form though. Since I was already hosting the website on Cloudflare's Pages product for static websites, I thought I would try their serverless workers out for the contact form. Cloudflare's Worker documentation is a work in progress &amp;amp; needs some love. I submitted a few PRs. It seemed that they were possibly doing a massive overhaul as well. To help anyone interested I'm adding this blog article. Hopefully, you get some use out of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free Everything
&lt;/h2&gt;

&lt;p&gt;What's awesome about the tools I ended up selecting for this project is that they all offer free developer plans for personal projects. If you're running a business, you'll have to look at each one in particular. But everything in this project is easy to swap in &amp;amp; out.&lt;/p&gt;

&lt;p&gt;Here's the scope of what we're building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static Website Hosting on &lt;a href="https://pages.cloudflare.com/"&gt;Cloudflare Pages&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;API to handle form submissions on &lt;a href="https://workers.cloudflare.com/"&gt;Cloudflare Workers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Logging with &lt;a href="https://sentry.io/"&gt;Sentry&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Transactional E-mails with &lt;a href="https://sendgrid.com/"&gt;SendGrid&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Captcha with &lt;a href="https://developers.google.com/recaptcha/"&gt;Google&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;You can pull the project from &lt;a href="https://github.com/mattferderer/cloudflare-contact-us-worker"&gt;GitHub&lt;/a&gt; &amp;amp; run &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a Static Website with a Form
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;sample.tsx&lt;/code&gt; file is in the root that shows how to use this project with a React front end. It should be simple to translate that to JS without a framework or your framework of choice. You will need to set up a free account on each provider unless you don't want to use that piece. This is the part of your website you would place on something like Cloudflare Pages.&lt;/p&gt;

&lt;p&gt;Here are some of the critical parts. For Google's Captcha, we need to add this to the header of your page. You will need to enter your unique captcha key from Google in &lt;code&gt;{GOOGLE_CAPTCHA_KEY}&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;async&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;{`https://www.google.com/recaptcha/api.js?render=${GOOGLE_CAPTCHA_KEY}`}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the form submit, we can attach an &lt;code&gt;onSubmit&lt;/code&gt; event listener like the following.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SyntheticEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grecaptcha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grecaptcha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_CAPTCHA_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CLOUDFLARE_WORKER_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;setShowAlert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nx"&gt;setSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nx"&gt;setErrorMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                    &lt;span class="nx"&gt;setShowAlert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nx"&gt;setSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nx"&gt;setErrorMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;setShowAlert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nx"&gt;setSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nx"&gt;setErrorMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendEmailFailedMsg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using onSubmit, this handler can catch the button with &lt;code&gt;type="submit"&lt;/code&gt; events &amp;amp; when a user presses the enter key in a form. The first step is to prevent the default action value of the form. Then use Google's grecaptcha to get a token to pass along with our request in an attempt to verify that a person submitted the form. After that, the method formats the body of the request to send to our Cloudflare Worker &amp;amp; then sends it as a POST request. Some of the variables being passed are set outside of this method, so checkout the &lt;code&gt;sample.tsx&lt;/code&gt; for more info. This method is also using a modal to show an error or success message based on the response from the Cloudflare Worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare Worker Code
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;src&lt;/code&gt; directory has the most important parts. Starting with the &lt;code&gt;index.tsx&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initSentry&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./logger&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handleRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sentry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;initSentry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handleRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sentry&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the event listener that Cloudflare Workers will run when it receives a HTTP request. The Sentry logging tool gets initialized here &amp;amp; gets passed in to the custom handleRequest method. The handleRequest method will generate the response once we're done.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.handle.tsx&lt;/code&gt; file has the bulk of our logic, including the handleRequest method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;handleRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sentry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Toucan&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isOptionsRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;handleOptionsRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contactRequest&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;validateRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sentry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;captureMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Invalid Request: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;invalidRequestResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; 

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contactRequest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;emailRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;captureMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Sendgrid request failed.
      Request: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contactRequest&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
      Response: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;emailRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;invalidRequestResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendEmailFailedMsg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;acceptedResponse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;captureException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;invalidRequestResponse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first step is to check if we're receiving an &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request"&gt;Options HTTP request&lt;/a&gt;. If that's the case, we respond with what types of HTTP requests we're accepting &amp;amp; end the task.&lt;/p&gt;

&lt;p&gt;Then we check if this is a valid request. This lives in the &lt;code&gt;validate.ts&lt;/code&gt;. It contains a bunch of simple checks looking to make sure it's a POST request &amp;amp; the necessary fields exist to complete this task. The Google Captcha validation lives in here as well. Let's look closer at that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validateCaptcha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ContactRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sentry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Toucan&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StatusMsg&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;captureMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Missing captcha token. Request:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sendEmailFailedMsg&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;captchaUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://www.google.com/recaptcha/api/siteverify?secret=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;RECAPTCHA_SECRET&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;response=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;captchaUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;CaptchaResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;captureMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid captcha response.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;getCaptchaResponseAndOriginalRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sendEmailFailedMsg&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;captchaScoreIsNotValid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;captureMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Low Captcha Score. &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;getCaptchaResponseAndOriginalRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sendEmailFailedMsg&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The captcha method makes sure there is a token passed by Google into the request. If not, it's logged via the Sentry logger &amp;amp; a response is sent back to the client. If the token exists, it gets verified by Google. Google then sends back a score from 0.0 to 1.0. In &lt;code&gt;captchaScoreIsNotValid&lt;/code&gt; I've set it to make sure the score is equal to or above 0.5. If everything looks good in the captcha, we move on to sending an e-mail in the &lt;code&gt;sendEmail.ts&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contactRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ContactRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.sendgrid.com/v3/mail/send&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formatSendGridRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contactRequest&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SENDGRID_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailRequest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simple request to SendGrid. Using a tool like SendGrid or &lt;a href="https://mailchimp.com/"&gt;MailChimp&lt;/a&gt; allows an easy way to send &amp;amp; track e-mail success rates. SendGrid will send us a success or a failure message with an error. If the message is a success our handleRequest method is done &amp;amp; can reply with a success message. If an error occurs, it gets logged via Sentry.&lt;/p&gt;

&lt;p&gt;Sentry is a very nice logging tool. If you pay for it, you can get integrations with &lt;a href="https://asana.com/"&gt;Asana&lt;/a&gt;, &lt;a href="https://github.com"&gt;GitHub&lt;/a&gt;, or a ton of other issue trackers. Sentry also has nice issue tracking.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Workflow
&lt;/h2&gt;

&lt;p&gt;A workflow file exists &lt;code&gt;.github\workflows\node.js.yml&lt;/code&gt; to build, run tests &amp;amp; deploy to Cloudflare. This makes deployments easy. For each place, you see a &lt;code&gt;${{ secrets.CF_VARIABLE_NAME }}&lt;/code&gt; you will need to add a corresponding 'Action Secret' in your GitHub repository. To do that, go to Settings and click Secrets (under Security), and then Actions. GitHub will replace your variables in the &lt;code&gt;node.js.yml&lt;/code&gt; file with the values you set here.&lt;/p&gt;

&lt;p&gt;It should be noted, I do have two variables that are not 'secrets' stored in the &lt;code&gt;node.js.yml&lt;/code&gt; file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RECIPIENT_NAME&lt;/li&gt;
&lt;li&gt;RECIPIENT_EMAIL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These two you can set a user secret for or you can just hard code into the config file. I like to hard code these because it allows me to easily look up the value &amp;amp; modify it. I also don't mind if someone else finds these values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare Worker
&lt;/h2&gt;

&lt;p&gt;When you create the Cloudflare Worker, you will need to go to settings and add the same variables that are in the GitHub Workflow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ACCESS_CONTROL_ALLOW_ORIGIN&lt;/li&gt;
&lt;li&gt;MAIL_SUBJECT&lt;/li&gt;
&lt;li&gt;RECAPTCHA_SECRET&lt;/li&gt;
&lt;li&gt;RECIPIENT_EMAIL&lt;/li&gt;
&lt;li&gt;RECIPIENT_NAME&lt;/li&gt;
&lt;li&gt;SENDGRID_API_KEY&lt;/li&gt;
&lt;li&gt;SENTRY_DSN&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You will also need to generate an API token to use in your GitHub workflow to use with the CF_API_TOKEN secret. You can generate a token by clicking on the 'My Profile' link once you're logged in on the upper right corner. Then go to API Tokens &amp;amp; generate a token with access to the following: Workers Tail:Read, Workers KV Storage:Edit, Workers Scripts:Edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback
&lt;/h2&gt;

&lt;p&gt;I wrote this tutorial rather swiftly &amp;amp; months after implementing the actual code. Please let me know if you found it valuable or if you have questions or found something I could improve on. I wouldn't mind spending more time building small apps with tutorials but I want to make sure people are finding value first.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally posted on &lt;a href="https://mattferderer.com/submit-a-static-website-form-with-cloudflare-workers"&gt;mattferderer.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>cloud</category>
    </item>
    <item>
      <title>How to Use Tailwind with Blazor</title>
      <dc:creator>Matt Ferderer</dc:creator>
      <pubDate>Tue, 01 Dec 2020 21:51:54 +0000</pubDate>
      <link>https://dev.to/mattferderer/how-to-use-tailwind-with-blazor-3401</link>
      <guid>https://dev.to/mattferderer/how-to-use-tailwind-with-blazor-3401</guid>
      <description>&lt;p&gt;Here are my preferred ways you can add Tailwind to your Blazor app.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
CDN

&lt;ul&gt;
&lt;li&gt;Pros: Quick &amp;amp; easy&lt;/li&gt;
&lt;li&gt;Cons: Large file size, no customization or advanced features&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
PostCSS

&lt;ul&gt;
&lt;li&gt;Pros: Lots of customization and features&lt;/li&gt;
&lt;li&gt;Cons: Requires NPM, more complicated&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Adding Tailwind with a CDN
&lt;/h2&gt;

&lt;p&gt;The easiest way is to drop a link to the CDN build in the header of your &lt;code&gt;wwwroot/index.html&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run into an error with the CSS not displaying due to "Resource interpreted as Stylesheet but transferred with MIME type text/plain" use a different CDN or a specific CSS version on unpkg.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/tailwindcss@2.0.1/dist/tailwind.min.css"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding Tailwind with PostCSS
&lt;/h2&gt;

&lt;p&gt;If you can use Node Package Manager as part of your build processes, add a Package.json file to your project at the root level with &lt;code&gt;npm init&lt;/code&gt; in the terminal.&lt;/p&gt;

&lt;p&gt;Add Tailwind and dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install tailwindcss postcss autoprefixer postcss-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a postcss.config.js file to the root level of your project with the following.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// postcss.config.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;tailwindcss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;autoprefixer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To customize the colors or other settings add a tailwind.config.js file to the root with the terminal command &lt;code&gt;npx tailwindcss init&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;This creates an object that overrides the &lt;a href="https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js"&gt;default config object&lt;/a&gt;. The &lt;a href="https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js"&gt;default config object&lt;/a&gt; is a great reference for how to override. In this case, we'll add a color to our config named brandBlue.&lt;/p&gt;

&lt;p&gt;First, require the colors object from tailwind at the top of your config. Copy and paste the default colors from the &lt;a href="https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js"&gt;default config&lt;/a&gt;. The brandBlue color can now be added.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;//tailwind.config.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tailwindcss/colors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;purge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;darkMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// or 'media' or 'class'&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;brandBlue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#2b59c0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transparent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currentColor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;black&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;black&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;white&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;white&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coolGray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;red&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;red&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;yellow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;green&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emerald&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;indigo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indigo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;purple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Side note, instead of using the Tailwind's &lt;a href="https://github.com/tailwindlabs/tailwindcss/blob/master/colors.js"&gt;color object&lt;/a&gt;, I recommend creating your own but using CSS variables. That makes it easier if you need to use a specific color in both Tailwind &amp;amp; custom CSS classes.&lt;/p&gt;

&lt;p&gt;In your main css file drop these three lines in at the top.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;utilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have any @import statements in your CSS, make sure they come first or PostCSS will get angry.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;package.json&lt;/code&gt; add a property to the scripts object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buildcss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postcss wwwroot/css/app.css -o wwwroot/css/app.min.css"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first path is your source CSS and the second is where you want to output it. If you're using the Blazor template that comes with .NET, you will need to tweak the index.html file to use app.min.css instead of app.css. Feel free to change these CSS file names as you see fit.&lt;/p&gt;

&lt;p&gt;Now you can run &lt;code&gt;npm run buildcss&lt;/code&gt; in the terminal to generate your CSS.&lt;/p&gt;

&lt;p&gt;Once done, you can search app.min.css for "brand". The CSS file should now have Tailwind CSS classes in it, including the brand color in multiple locations.&lt;/p&gt;

&lt;p&gt;The file size has grown a bit now. It's still smaller than most images but to be good people, we can remove unused CSS.&lt;/p&gt;

&lt;p&gt;In the tailwind.config.js file, update the purge property to include your Razor and HTML files. I'm also setting &lt;code&gt;enabled&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;. Without this set, &lt;code&gt;NODE_ENV&lt;/code&gt; must be set to &lt;code&gt;production&lt;/code&gt; for CSS to be removed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;//tailwind.config.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tailwindcss/colors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;purge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./**/*.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.**/*.razor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;darkMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// or 'media' or 'class'&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;brandBlue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#2b59c0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transparent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currentColor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;black&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;black&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;white&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;white&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coolGray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;red&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;red&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;yellow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;green&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emerald&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;indigo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indigo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;purple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Tailwind will use &lt;a href="https://purgecss.com/"&gt;PurgeCSS&lt;/a&gt; to remove any Tailwind classes that do not exist in a Razor or HTML page in the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback Appreciated
&lt;/h2&gt;

&lt;p&gt;I would love to hear in the comments your thoughts on using NPM with .NET. I'm trying to understand better where most .NET devs frontend stacks are.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally posted on &lt;a href="https://mattferderer.com/tailwind-with-blazor"&gt;mattferderer.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blazor</category>
      <category>dotnet</category>
      <category>css</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Scrape a Website &amp; Send an E-mail with Azure Functions</title>
      <dc:creator>Matt Ferderer</dc:creator>
      <pubDate>Wed, 27 Jun 2018 20:12:23 +0000</pubDate>
      <link>https://dev.to/mattferderer/scrape-a-website--send-an-e-mail-with-azure-functions-1bo7</link>
      <guid>https://dev.to/mattferderer/scrape-a-website--send-an-e-mail-with-azure-functions-1bo7</guid>
      <description>&lt;p&gt;Serverless Functions are an awesome way to create small tasks that can run on a schedule, by the click of a button or using your voice. We'll create a function that visits a website, collects important data &amp;amp; sends that via an e-mail back to us on a schedule.&lt;/p&gt;

&lt;p&gt;This project will use Azure Functions. They are dirt cheap, especially for this project. Like most serverless architectures, you only pay for the server time it takes your function to run.&lt;/p&gt;

&lt;p&gt;If you don't have an Azure account, a quick search for "Free Azure Account" should take you to Microsoft's latest offering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create an Azure Function
&lt;/h2&gt;

&lt;p&gt;Login to Azure and click to &lt;em&gt;Create a Resource&lt;/em&gt;. Here you can search for &lt;em&gt;Functions&lt;/em&gt; and add a &lt;em&gt;Function App&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;On the next screen give the app a name, select a resource group, etc. If you're new to this, drop me a line in the comments &amp;amp; I can walk you through these steps. Once done, click &lt;em&gt;Create&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Once Azure has the new resources ready, you can either click the pop up that appears or go to &lt;em&gt;All Resources&lt;/em&gt; and find the Function App there. The Function App has a lightning bolt icon.&lt;/p&gt;

&lt;p&gt;On the next screen, hover over &lt;em&gt;Functions&lt;/em&gt; on the left menu. Click the + sign that appears to create our Function.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmattferderer.com%2Fscrape-a-website-and-send-an-email-with-azure-functions-1-a5fcd4a6fa8df3255c01472832f3aef8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmattferderer.com%2Fscrape-a-website-and-send-an-email-with-azure-functions-1-a5fcd4a6fa8df3255c01472832f3aef8.gif" alt="Azure Functions Overview Screen."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select &lt;em&gt;Timer&lt;/em&gt; if you want to do this on a schedule or &lt;em&gt;Webhook+API&lt;/em&gt; if you want a URL to send a request to so you can do it on demand. Also make sure C# is selected as the language below.&lt;/p&gt;

&lt;p&gt;On the right side, click View Files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmattferderer.com%2Fscrape-a-website-and-send-an-email-with-azure-functions-2-48c8a11a31647280d9436a0e12eab44f.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmattferderer.com%2Fscrape-a-website-and-send-an-email-with-azure-functions-2-48c8a11a31647280d9436a0e12eab44f.gif" alt="Click View Files on the right side of the screen."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add a new &lt;code&gt;project.json&lt;/code&gt; file and add the following text. Then click the Save button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "frameworks": {
    "net46":{
      "dependencies": {
        "HtmlAgilityPack": "1.8.2"
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: Currently only &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-csharp#using-nuget-packages" rel="noopener noreferrer"&gt;.NET Framework 4.6 is supported on Azure Functions&lt;/a&gt;. .Net Core is available in preview. If at any later time you wish to use .Net Core instead, adjust the framework snippet above. The following code should require minimum changes if any.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For this demo &amp;amp; purposes of legality, we'll scrape &lt;a href="https://mattferderer.com" rel="noopener noreferrer"&gt;my blog's homepage&lt;/a&gt;. Here's the code I wrote. If you're looking to make multiple asynchronous requests, the comment in the GetLatestArticles function explains how.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#r "SendGrid"
using System.Net;
using HtmlAgilityPack;
using SendGrid.Helpers.Mail;

// This is our main method that's called when the function runs. Azure passes the TimerInfo object and a TraceWriter object to output logs. We use C#'s out parameter modifier to work with Azure's Sendgrid e-mail integration using the Mail class.
public static void Run(TimerInfo myTimer, TraceWriter log, out Mail message)
{
    try{
        var latestArticle = GetLatestArticles().Result;
        log.Info(latestArticle);
        message = SendEmail(latestArticle);
    } catch (Exception e) {
        log.Info(e.ToString());
        message = SendEmail(e.ToString());
    }

}

public static async Task&amp;lt;string&amp;gt; GetLatestArticles() {
    var mattsLatestArticle = await GetMattsLatestArticle();
    /**
     * If you want to scrape additional resources with async, 
     * add more functions like above.
     *
     * Then return them like this:
     * return mattsLatestArticle + otherLatestArticle;
     */
    return mattsLatestArticle;
}

public static async Task&amp;lt;string&amp;gt; GetMattsLatestArticle() {
    string website = "https://mattferderer.com";

    HttpClient client = new HttpClient();
    string html = await client.GetStringAsync(website);

    HtmlDocument doc = new HtmlDocument();
    doc.LoadHtml(html); 

    var article = doc.DocumentNode
        .SelectNodes("//a")
        .FirstOrDefault(x =&amp;gt; x.Attributes.Contains("class") &amp;amp;&amp;amp; x.Attributes["class"].Value.Contains("article-link"));

    var url = article
        .Attributes
        .FirstOrDefault(x =&amp;gt; x.Name == "href")
        .Value;

    var title = article
        .SelectSingleNode("//h2")
        .InnerHtml;

    return $"&amp;lt;p&amp;gt;&amp;lt;a href=\"{website}{url}\"&amp;gt;{title}&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;";
}

public static Mail SendEmail(string input) {
        var message = new Mail
    {
        Subject = "Latest Article"
    };

    var personalization = new Personalization();
    personalization.AddTo(new Email("yourEmail@example.com"));

    var content = new Content
    {
        Type = "text/html",
        Value = input
    };
    message.AddContent(content);
    message.AddPersonalization(personalization);
    return message;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add SendGrid Integration
&lt;/h2&gt;

&lt;p&gt;Before this will send an e-mail it needs an e-mail service integrated with the Azure account. I recommend &lt;a href="https://sendgrid.com" rel="noopener noreferrer"&gt;SendGrid&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sendgrid.com" rel="noopener noreferrer"&gt;SendGrid&lt;/a&gt; is easy to integrate with Azure &amp;amp; a has a free developer plan. They're also a great provider in general. For these reasons, they're my go to for fun apps &amp;amp; production apps.&lt;/p&gt;

&lt;p&gt;Create a &lt;a href="https://sendgrid.com" rel="noopener noreferrer"&gt;SendGrid&lt;/a&gt; account and then go to settings &amp;gt; API Keys. Click create an API Key. Give it a name. Restricted access with &lt;code&gt;Mail Send&lt;/code&gt; is all we need for permissions. Copy your API key somewhere safe.&lt;/p&gt;

&lt;p&gt;Back in Azure, open the Functions Application Settings by clicking the app's name on the left menu. Then click Application Settings near the bottom under Configured features. (See highlighted links below)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmattferderer.com%2Fscrape-a-website-and-send-an-email-with-azure-functions-3-b415cb520c04975e6c3f28dbef8b5f47.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmattferderer.com%2Fscrape-a-website-and-send-an-email-with-azure-functions-3-b415cb520c04975e6c3f28dbef8b5f47.gif" alt="Click Application Settings under Configured features"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scroll down to the Application Settings, above Connection strings and click Add new setting.&lt;/p&gt;

&lt;p&gt;Add the key &lt;code&gt;SendGridKey&lt;/code&gt;. For the value, add the API key Sendgrid gave you. Click save.&lt;/p&gt;

&lt;p&gt;Under the functions name on the left, click Integrate &amp;amp; add a new Output.&lt;/p&gt;

&lt;p&gt;Select SendGrid from the list of Output options.&lt;/p&gt;

&lt;p&gt;In the SendGrid API Key drop down menu on the next screen, select SendGridKey.&lt;/p&gt;

&lt;p&gt;Fill out the From address &amp;amp; subject as well if you want defaults for the function. Make sure to Save.&lt;/p&gt;

&lt;p&gt;The output settings will now be added to the function.json file that Azure created for you if you did a Timer Trigger app. You can also find the schedule using a Cron pattern in there.&lt;/p&gt;

&lt;p&gt;At this point, you should be able to run the Azure Function &amp;amp; have it send you an e-mail. If not, let me know in the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Notes
&lt;/h2&gt;

&lt;p&gt;If you're sending links in the e-mail, as done in the above example, you may find it useful to &lt;a href="https://sendgrid.com/docs/User_Guide/Settings/tracking.html" rel="noopener noreferrer"&gt;turn off SendGrid's "Tracking Settings"&lt;/a&gt; feature. This feature changes your links &amp;amp; sometimes breaks them if you don't have this setup right. You can disable it on Sendgrid's Admin screen by going to Settings &amp;gt; Tracking.&lt;/p&gt;

&lt;p&gt;You can debug your code locally using &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions" rel="noopener noreferrer"&gt;VS Code&lt;/a&gt; or &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-vs" rel="noopener noreferrer"&gt;Visual Studio&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>net</category>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Hosting Blazor on Netlify</title>
      <dc:creator>Matt Ferderer</dc:creator>
      <pubDate>Thu, 21 Jun 2018 20:12:23 +0000</pubDate>
      <link>https://dev.to/mattferderer/hosting-blazor-on-netlify-p8</link>
      <guid>https://dev.to/mattferderer/hosting-blazor-on-netlify-p8</guid>
      <description>

&lt;p&gt;Since Blazor is a frontend framework, we can host our Blazor apps on any serverless or static web host. The only requirement is that we need to add minor configuration to redirect URLs so that all URLs point to our index.html page. &lt;a href="https://www.netlify.com/"&gt;Netlify&lt;/a&gt; fits this perfect. &lt;a href="https://www.netlify.com/"&gt;Netlify&lt;/a&gt; also happens to be my favorite host for static websites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://go.microsoft.com/fwlink/?linkid=873092"&gt;.NET Core 2.1 or later&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Blazor templates should be installed. &lt;code&gt;dotnet new -i Microsoft.AspNetCore.Blazor.Templates&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Create a Demo Blazor App
&lt;/h2&gt;

&lt;p&gt;Create a new Blazor app with the command: &lt;code&gt;dotnet new blazor -o staticDemo&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Make sure your app works by running &lt;code&gt;dotnet run&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Netlify Specific Steps
&lt;/h2&gt;

&lt;p&gt;Add a file named &lt;code&gt;_redirects&lt;/code&gt; in your &lt;code&gt;wwwroot&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;Add the following line to your &lt;code&gt;_redirects&lt;/code&gt; file:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/*    /index.html   200
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This redirects any URL to the index.html file. Since Blazor is a SPA tool, if someone enters a URL such as oursite.com/counter we don't want the server to look for a counter file. Instead the server should direct the request to index.html which will take care of the routing.&lt;/p&gt;

&lt;p&gt;If you publish on another static host or a serverless environment, check their docs for how to write a similar redirect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing to Netlify
&lt;/h2&gt;

&lt;p&gt;Once we're done with that, the easiest way to deploy to &lt;a href="https://www.netlify.com/"&gt;Netlify&lt;/a&gt; is to add this to a GitHub repo.&lt;/p&gt;

&lt;p&gt;Netlify can't build this project for us since they can't build dotnet. We'll need to handle building &amp;amp; adding the files to our repository.&lt;/p&gt;

&lt;p&gt;By default the .gitignore file created with our project ignores the default build directory. You can modify your .gitignore file to not ignore the default release directory. Then you can run &lt;code&gt;dotnet publish -c release&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Another option is to publish to a different directory using the &lt;code&gt;-o&lt;/code&gt; parameter. Here's an example: &lt;code&gt;dotnet publish -c release -o ./dist&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Once you've done either of the above, push your changes to a GitHub repository.&lt;/p&gt;

&lt;p&gt;After that you can go to Netlify &amp;amp; create a new site from your GitHub repository. After linking your site to your repo, you're prompted with deploy settings. You can ignore the build command. For the publish directory, add the directory you published to. It should look like this:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_framework
css
sample-data
_redirects
index.html
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Every time you push changes to your repository, Netlify will re-deploy them.&lt;/p&gt;

&lt;p&gt;If you run into issues or you deploy this elsewhere, let me know in the comments below.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally posted on &lt;a href="https://mattferderer.com/host-blazor-on-netlify"&gt;mattferderer.com&lt;/a&gt;.&lt;/em&gt;            &lt;/p&gt;


</description>
      <category>blazor</category>
      <category>csharp</category>
      <category>staticsites</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Add Coding Symbols to VSCode</title>
      <dc:creator>Matt Ferderer</dc:creator>
      <pubDate>Sun, 17 Jun 2018 20:12:23 +0000</pubDate>
      <link>https://dev.to/mattferderer/add-coding-symbols-to-vscode-12pl</link>
      <guid>https://dev.to/mattferderer/add-coding-symbols-to-vscode-12pl</guid>
      <description>&lt;p&gt;As developers we spend more time reading code, that writing. One of the most common tasks we do are compare items. Using symbols can make your code easier &amp;amp; more enjoyable to read.&lt;/p&gt;

&lt;p&gt;Here's an example of how adding coding symbols, also known as font ligatures look.&lt;/p&gt;

&lt;p&gt;&lt;a href="///static/code-symbol-preview-2ab22531392495cde253634d93667edf-25de8.jpg"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LJFvuz5c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://mattferderer.com/static/code-symbol-preview-2ab22531392495cde253634d93667edf-25de8.jpg" alt="VS Code using Fira Code font ligatures."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To add this to your VS Code, do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Install the &lt;a href="https://github.com/tonsky/FiraCode"&gt;Fira Code font&lt;/a&gt; on your computer.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Edit your user settings in VS Code to use Font Ligatures and the 'Fira Code' font.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"editor.fontFamily": "'Fira Code', Consolas, 'Courier New', monospace, 'Segoe UI Emoji'",
"editor.fontLigatures": true,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! You're done! Enjoy an improved coding experience!&lt;/p&gt;

</description>
      <category>vscode</category>
    </item>
    <item>
      <title>What is CSP? Why &amp; How to Add it to Your Website.</title>
      <dc:creator>Matt Ferderer</dc:creator>
      <pubDate>Sun, 13 May 2018 03:59:47 +0000</pubDate>
      <link>https://dev.to/mattferderer/what-is-csp-why--how-to-add-it-to-your-website-28df</link>
      <guid>https://dev.to/mattferderer/what-is-csp-why--how-to-add-it-to-your-website-28df</guid>
      <description>&lt;p&gt;Cross-Site Scripting (XSS) sucks! XSS is when someone sneaks JavaScript or CSS into your site through a comment, a form, an advertisement or a NPM package that's part of your JavaScript build. Now they own every user's account that is visiting your website. XSS is such a common attack, &lt;a href="https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project" rel="noopener noreferrer"&gt;OWASP claims it happens in 2 out of every 3 websites &amp;amp; apps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can eliminate most XSS attacks with a CSP (Content Security Policy). A CSP lets you list external and internal scripts, styles, images and other content sources to allow. It's even compatible with &lt;a href="https://caniuse.com/#search=Content%20Security%20Policy" rel="noopener noreferrer"&gt;all the major browsers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since CSP can block one of the most common attacks known you think everyone would be using it, right? Nope! &lt;a href="https://scotthelme.co.uk/alexa-top-1-million-analysis-february-2018/" rel="noopener noreferrer"&gt;Less than 2.5% of the top million visited sites use it&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="http://dilbert.com/strip/2018-05-08" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhqeuczc4fv9dngqb5h9x.gif" alt="Dilbert Comic. Click link for a transcript." width="900" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For most websites security is an afterthought, until someone steals all their data. Then the public rages on social media. The typical company response is to fire someone &amp;amp; promise to put security first, all while crossing their fingers behind their back.&lt;/p&gt;

&lt;p&gt;Let's take a look at how we can avoid such a mess.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Add a CSP Policy
&lt;/h2&gt;

&lt;p&gt;The first step is to add a &lt;a href="https://mattferderer.com/how-to-add-a-header" rel="noopener noreferrer"&gt;header to your server configuration&lt;/a&gt;. It's recommended to start with the strictest CSP rule possible but set it to "report only" mode. This creates a report on what would happen if we blocked everything possible. Once you have your report you can start picking which items you want to allow (aka whitelist), which items to create alternate fixes for and which items to block.&lt;/p&gt;

&lt;p&gt;Here's a recommended header to start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content-Security-Policy-Report-Only: default-src 'none'; form-action 'none'; frame-ancestors 'none';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falobxmlb0cmt3tlgdh14.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falobxmlb0cmt3tlgdh14.png" alt="Once you've added the CSP to your website, you can use your browser developer tools to view it &amp;amp; any other headers." width="590" height="226"&gt;&lt;/a&gt; Your CSP should appear along with your other headers when viewing your page in the browser's developer tools.&lt;/p&gt;

&lt;p&gt;If we didn't set it to report mode, you would see &lt;em&gt;"The full power of CSP!"&lt;/em&gt; In other words, the CSP would block most of your website.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remember, the role of a Content Security Policy (CSP) is to block everything you haven't allowed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you open up the console in your browser developer tools (F12) you typically will see a lot of errors. The first error might complain about lacking a report-uri but we'll get to that later. The rest of the errors should all start with [Report Only]. This is the CSP report mode letting you know what content would be blocked &amp;amp; how to allow it.&lt;/p&gt;

&lt;p&gt;A CSS stylesheet is often one of the first errors that will appear. It will look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Report Only] Refused to load the stylesheet 'https://example.com/style.css' because it violates the following Content Security Policy directive: "default-src 'none'". Note that 'style-src' was not explicitly set, so 'default-src' is used as a fallback.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To fix this, we adjust our policy by adding a style-src directive that allows 'self'. Adding 'self' allows us to include any CSS stylesheet hosted on the same URL &amp;amp; port number as our page. Without doing this, style-src defaults to the default-src directive which we have set to none.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content-Security-Policy-Report-Only: default-src 'none'; form-action 'none'; frame-ancestors 'none'; style-src 'self';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Odds are high that you will also want to add 'self' to images and scripts. This would result in adjusting our CSP again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content-Security-Policy-Report-Only: default-src 'none'; form-action 'none'; frame-ancestors 'none'; style-src 'self'; script-src 'self'; img-src 'self';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typical website needs to require external scripts as well. We can allow JavaScript from the domain &lt;a href="https://cdnjs.com/" rel="noopener noreferrer"&gt;cdnjs.com&lt;/a&gt; by modifying our script-src directive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content-Security-Policy-Report-Only: default-src 'none'; form-action 'none'; frame-ancestors 'none'; style-src 'self'; script-src 'self' cdnjs.com; img-src 'self';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thought you might have looking at our current rule is why did we explicitly declare directives for form-action &amp;amp; frame-ancestors? They are special cases that do not use the default-src fallback.&lt;/p&gt;

&lt;p&gt;You can find the entire list of directives on &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#Directives" rel="noopener noreferrer"&gt;MDN&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inline Script is My Middle Name
&lt;/h2&gt;

&lt;p&gt;Inline JavaScript and CSS are often used on websites but they are also dangerous. They're the easiest way for a hacker to create an XSS attack. Therefore, to allow them you must add &lt;code&gt;'unsafe-inline'&lt;/code&gt; to the directive you want to allow them for. This is to make sure you understand what you're doing isn't recommended.&lt;/p&gt;

&lt;p&gt;A common CSS pattern is to inline your most important CSS that renders your 'above fold content' with &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tags. This can help decrease the perceived rendering time. With &lt;a href="https://mattferderer.com/switch-to-http2-the-easiest-way-to-speed-up-your-site" rel="noopener noreferrer"&gt;HTTP/2&lt;/a&gt; I  often argue against doing this as it &lt;a href="https://next.smashingmagazine.com/2017/04/guide-http2-server-push/#test-outcomes" rel="noopener noreferrer"&gt;tends to be slower&lt;/a&gt;. If you do choose to use inline scripts, you have three options.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get a SHA-256 hash of the script &amp;amp; add it to our CSP. Chrome's dev tools will even generate a SHA-256 for you in the console when it displays the CSP error. Adding it to our current CSP example would look like this:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content-Security-Policy-Report-Only: default-src 'none'; form-action 'none'; frame-ancestors 'none'; style-src 'self'; script-src 'self' cdnjs.com sha256-c2u0cNUv1GcIb92+ybgJ4yMPatX/k+xxHHbugKVGUU8=; img-src 'self';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Generate a unique nonce by your server for each inline script. This means each server request has to generate a new nonce. You would add it to your CSP the same way as a SHA-256, &lt;code&gt;nonce-47c2gtf3a1&lt;/code&gt;. You also need to add it to the script tag: &lt;code&gt;&amp;lt;script nonce="47c2gtf3a1"&amp;gt;&lt;/code&gt;. It's rare to see nonces used as they are rather inconvenient to implement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Allow inline scripts by adding &lt;code&gt;'unsafe-inline'&lt;/code&gt; to your policy &amp;amp; hang your head in shame. This allows inline scripts, which weakens your CSP &amp;amp; allows XSS attacks. Doing this should cause you to feel some sadness.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Cheer Up! Unsafe Inline Isn't the End of the World! Just Your Company. (Just kidding, sort of...)
&lt;/h2&gt;

&lt;p&gt;When first implementing your CSP, there is a good chance you will need to use unsafe-inline on either your style-src or script-src directives. You might even need to allow JavaScript's eval function with unsafe-eval. &lt;em&gt;Gasp!&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Just remember, a CSP should be one of many weapons in your security arsenal. It should not be your only weapon.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Having a CSP with a few unsafe rules is still better than not having a CSP at all.&lt;/strong&gt; Not implementing a CSP at all would be the same as setting every directive to allow all of the unsafe CSP rules. &lt;/p&gt;

&lt;p&gt;For example, a common way to steal logins using CSS is by sending a request for a background image or font to an evil URL such as &lt;code&gt;http://evilexamplesite.com?password=a&lt;/code&gt; where &lt;code&gt;a&lt;/code&gt; is the letter you typed into the password login field. When you would type the next letter of your password, the evil CSS script would send another request but with that letter instead of &lt;code&gt;a&lt;/code&gt;. The evil site then logs these requests to determine your username &amp;amp; password. By allowing unsafe-inline for our style-src, someone could inject this evil code. Fortunately, their code wouldn't work since our CSP doesn't allow img-src &amp;amp; font-src from the evil example site.&lt;/p&gt;

&lt;p&gt;You are also not in bad company by doing this. A lot of sites, including &lt;a href="https://github.com" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; &amp;amp; security professional &lt;a href="https://troyhunt.com" rel="noopener noreferrer"&gt;Troy Hunt's blog&lt;/a&gt; use unsafe-inline. Facebook uses unsafe-eval &amp;amp; even requires it for some of their SDKs. Anyone using Google Tag Manager for analytics will also have to reduce their CSP security. I must confess as well. I use &lt;a href="https://gatsbyjs.org/" rel="noopener noreferrer"&gt;GatsbyJS&lt;/a&gt; for my personal blog &amp;amp; there are issues that need to be fixed before I can remove unsafe-inline. &lt;/p&gt;

&lt;p&gt;If you still feel defeated for having to succumb to implementing an unsafe rule on a directive, you can try applying a different CSP header to each page on your website. If you have areas that allow or display input from a user or outside source, you could try adding a stricter CSP on those pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating the Rest of Your CSP
&lt;/h2&gt;

&lt;p&gt;The old-fashioned way of doing this is to go to each page on your site, check for these errors &amp;amp; fix them. If you have the free time to do it this way, great! You might even enjoy this nice &lt;a href="https://chrome.google.com/webstore/detail/csp-mitigator/gijlobangojajlbodabkpjpheeeokhfa" rel="noopener noreferrer"&gt;Chrome extension&lt;/a&gt; or the &lt;a href="https://github.com/david-risney/CSP-Fiddler-Extension" rel="noopener noreferrer"&gt;Fiddler extension&lt;/a&gt;. They let you browse your website &amp;amp; will generate a proper CSP for you. &lt;/p&gt;

&lt;p&gt;When I first learned about CSP over a year ago, this task seemed to daunting for me to complete. It was then that I learned about the &lt;code&gt;report-uri&lt;/code&gt; feature of CSP. You can add a &lt;code&gt;report-uri&lt;/code&gt; to the end of your CSP with an URL to send reports to. The browser will send any CSP violation to the URL you specify. Now your visitors can do all that work for you just by using your website.&lt;/p&gt;

&lt;p&gt;Here is an example of an error the browser would send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"csp-report"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"document-uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mattferderer.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"referrer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"violated-directive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"script-src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"effective-directive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"script-src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"original-policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default-src 'none'; form-action 'none'; frame-ancestors 'none'; report-uri https://mattferderer.report-uri.com/r/d/csp/wizard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"disposition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"report"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"blocked-uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"inline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"line-number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"source-file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mattferderer.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"status-code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"script-sample"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The report lets you know what happened &amp;amp; where.&lt;/p&gt;

&lt;p&gt;If you already have an error logging service, you could use that. If you're looking for a free &amp;amp; easy method to get started though, I recommend &lt;a href="https://report-uri.com/" rel="noopener noreferrer"&gt;Report URI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The fine folks at &lt;a href="https://www.talasecurity.io/" rel="noopener noreferrer"&gt;Tala Security&lt;/a&gt;, who run a similar service, pointed out to me a while back that a CSP is not a set it &amp;amp; forget it tool. As you update your site or the services you rely on get updated, your CSP may need to adjust. This makes the reporting service even more valuable.&lt;/p&gt;

&lt;p&gt;Having a reporting service will also show you real-world data from your users who are running different browsers, browser extensions, etc.&lt;/p&gt;

&lt;p&gt;I recommend running your CSP in report only mode &amp;amp; sending your reports to a service until you are confident you aren't blocking any valuable content from your users. Once done, you can change your CSP from &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; to &lt;code&gt;Content-Security-Policy&lt;/code&gt;. This will start enforcing your CSP. You can still keep the report-uri as part of your CSP to continue collecting errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start Guide
&lt;/h2&gt;

&lt;p&gt;Here's a quick recap on how to get started, with additional instructions using &lt;a href="https://report-uri.com/" rel="noopener noreferrer"&gt;Report URI&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Add a strict CSP Header to your site. I suggest &lt;code&gt;Content-Security-Policy-Report-Only: default-src 'none'; form-action 'none'; frame-ancestors 'none';&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sign up for a free account at &lt;a href="https://report-uri.com/" rel="noopener noreferrer"&gt;Report URI&lt;/a&gt;. &lt;em&gt;Make sure to verify your email.&lt;/em&gt; The service won't work until you do that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Using Report URI, go to Setup &amp;amp; create a CSP reporting address. Add that reporting address to your CSP: &lt;code&gt;Content-Security-Policy-Report-Only: default-src 'none'; form-action 'none'; frame-ancestors 'none'; report-uri https://yoursite.report-uri.com/r/d/csp/reportOnly&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Using Report URI, go to CSP &amp;gt; My Policies. Add a new policy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Using Report URI, go to CSP &amp;gt; Wizard. Watch as your data rolls in.* You can allow or block a site for each directive here. This will generate your policy for you. You can view it by going back to "My Policies".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Update your CSP with the new policy generated by Report URI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once you've run your CSP in report only mode &amp;amp; are satisfied with the lack of new entries showing up in Report URI, adjust your CSP from &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; to &lt;code&gt;Content-Security-Policy&lt;/code&gt; to begin enforcing your CSP.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;*&lt;em&gt;Depending on your reporting service, inline scripts that break the CSP policy may not show up.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Notes
&lt;/h2&gt;

&lt;p&gt;Twitter has a great package for Ruby on Rails developers to set &lt;a href="https://github.com/twitter/secure_headers" rel="noopener noreferrer"&gt;secure default headers&lt;/a&gt;. They also list &lt;a href="https://github.com/twitter/secure_headers#similar-libraries" rel="noopener noreferrer"&gt;similar libraries for other popular frameworks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://githubengineering.com/githubs-csp-journey/" rel="noopener noreferrer"&gt;GitHub's CSP Journey&lt;/a&gt; is a great article on the issues they faced while implementing their CSP.&lt;/p&gt;

&lt;p&gt;This &lt;a href="https://content-security-policy.com/" rel="noopener noreferrer"&gt;CSP Quick Reference Guide&lt;/a&gt; &amp;amp; Scott Helme's &lt;a href="https://scotthelme.co.uk/csp-cheat-sheet/" rel="noopener noreferrer"&gt;CSP Cheat Sheet&lt;/a&gt; are excellent resources to glance at when implementing a CSP.&lt;/p&gt;

&lt;p&gt;If you are in the need of extra precautions for specific pages, check out &lt;a href="https://html.spec.whatwg.org/dev/origin.html#sandboxing" rel="noopener noreferrer"&gt;Sandbox mode&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally posted on &lt;a href="https://mattferderer.com/what-is-csp-and-how-to-add-it-to-your-website" rel="noopener noreferrer"&gt;mattferderer.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
    </item>
  </channel>
</rss>
