Website - sideofburritos.com

Wednesday, April 27, 2022
Any notes/configurations I have related to sideofburritos.com.

🔗Newsletter sign-up form

🔗Background

sideofburritos.com is "technically" a static site hosted on Cloudlfare Pages. Since it's a static site, it can not handle POST requests which is what a sign-up form uses. To process the POST requests, I have to use a Cloudflare Worker which receives the POST request, extracts the email data, and then generates another POST request to the Buttondown API where that email is added to the subscriber list.

🔗Form template for homepage

The form setup instructions will only apply if you use Doks which is a Hugo theme template.

Copy the Newsletter partial code. Create a new file in layoutspartialssidebarnewsletter.html. If any of those directories don't exist, create them. After you paste the code you copied above, remove the unused fields (name and page) and update the rest of the text to whatever you like.

🔗Form template for posts pages

The homepage and posts page use different templates. To add the template for posts, we need to create a new file in layoutspartialsblogsingle.html. This is the layout that will then be used when creating a new blog post.

{{ define "main" }}
<div class="row justify-content-center">
  <div class="col-md-12 col-lg-10 col-xl-8">
    <article>
      <div class="blog-header">
        <h1>{{ .Title }}{{ with .Params.emoji -}}&nbsp;{{ . | emojify }}{{ end -}}</h1>
        {{ partial "main/blog-meta.html" . }}
      </div>
      <p class="lead">{{ .Params.lead | safeHTML }}</p>
      {{ .Content }}
    </article>
  </div>
</div>
{{ end }}

{{ define "sidebar-footer" }}
<section class="section section-sm mt-n5 mb-3">
  <div class="container">
    <div class="row justify-content-center">
      <div class="col-md-12 col-lg-10 col-xl-8">
        {{ partial "sidebar/newsletter.html" . -}}
      </div>
    </div>
  </div>
</section>
{{ end }}

🔗Add form to homepage

Now that we have the form template, we need to add it to our homepage. Open the file index.html which is in the layouts folder.

Add the following code right before the last {{ end }}.

<section class="section section-sm mt-n3 mb-3">
  <div class="container">
    <div class="row justify-content-center">
      <div class="col-md-12 col-lg-10 col-xl-8">
        {{ partial "sidebar/newsletter.html" . -}}
      </div>
    </div>
  </div>
</section>

At this point you will now see the newsletter form on your homepage, but it won't do anything.

🔗Custom SCSS for form

Since the default doks template doesn't have the scss for the form, we have to add it from the getdoks.org GitHub. We need to create custom scss to format the newsletter box correctly. The first step is to create the necessary folders . If these don't exist, create them - assetsscsscommon.

The first file we're going to add is app.scss in assetsscss. This file will load any of the custom scss we have in the common folder we created above. Add the code from the following link to app.scss - https://github.com/h-enk/doks/blob/master/assets/scss/app.scss

Once we have that file created, the last step is to add the custom scss. Create the file _custom.scss in assetsscsscommon.

Copy the code from https://github.com/h-enk/getdoks.org/blob/master/assets/scss/layouts/_posts.scss into _custom.scss.

🔗Form JavaScript

Without JavaScript, the form won't know what to do when someone enters and email and hist Subscribe. The following code needs to be added to assetsjsapp.js. If any of the directories don't exist, create them.

const processForm = form => {
  const data = new FormData(form)
  data.append('form-name', 'newsletter');
  fetch('https://sideofburritos.com/signup', {
    method: 'POST',
    body: data,
  })
  .then((response) => {
    if (response.ok) {
      console.log(data)
      form.innerHTML = '<p class="form--success"><strong>Nice.</strong> Check your inbox for a confirmation e-mail.</p>';
    } else {
      form.innerHTML = '<p class="form--error"><strong>Error:</strong> If you didn\'t expect an error, please <a href="contact/">let me know.</a></p>';
    }
  })
}

const emailForm = document.querySelector('.email-form')
if (emailForm) {
  emailForm.addEventListener('submit', e => {
    e.preventDefault();
    processForm(emailForm);
  })
}

The following code will read the form data once Subscribe is pressed. It will package the data up in a POST request and send it to https://sideofburritos.com/signup (that needs to be changed to whatever the Cloudflare Worker route we configure in the next section). There is also some error checking that will verify the email was added or if some other type of error occurred.

In my first version of the code above I was unable to see the properties of the response that was returned by the Worker below. This meant I was unable to perform error checking based on the response code. The reason for this is that I was using the option mode: 'no-cors' because my Worker wasn't returning CORS headers.

In addition, JavaScript may not access any properties of the resulting Response. This ensures that ServiceWorkers do not affect the semantics of the Web and prevents security and privacy issues arising from leaking data across domains. You can also update the messages the form displays to the user.

Source: https://developer.mozilla.org/en-US/docs/Web/API/Request/mode

We now have the form on our homepage and we have the JavaScript that will handle the data submitted. Next we'll be configuring the Cloudflare Worker that will process the POST request generated by the JavaScript above.

🔗Cloudflare Worker

frosty-mouse

Cloudflare Workers allow you to write and deploy "serverless" code. I put serverless in quotes because it's technically not serverless...it's just not your server.

The following code is what we'll use for our Worker.

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

const HOME_URL = "https://sideofburritos.com"

async function handleRequest(request) {
  const url = new URL(request.url)

  if (url.pathname === "/signup") {
    return submitHandler(request)
  }

  return Response.redirect(HOME_URL)
}

const submitHandler = async request => {
  if (request.method !== "POST") {
    return new Response("I'm a teapot", {
      status: 418
    })
  }

  const body = await request.formData();

  const {
    email,
  } = Object.fromEntries(body)

  const reqBody = {
    email
  }

  const response = await createButtondownSubscriber(reqBody)
  const responseHeaders = new Headers(response.headers)
  responseHeaders.set('Access-Control-Allow-Origin', 'https://sideofburritos.com'),
  responseHeaders.set('Access-Control-Allow-Methods', 'GET')
  if (response.status === 201) {
    return new Response(response.statusText, {status: response.status, headers: responseHeaders})
  } else {
    return new Response(response.statusText, {status: response.status, headers: responseHeaders})
  }
}

const createButtondownSubscriber = body => {
  return fetch(`https://api.buttondown.email/v1/subscribers`, {
    method: 'POST',
    body: JSON.stringify(body),
    headers: {
      'Authorization': `Token ${BUTTONDOWN_API_KEY}`,
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    }
  })
}

BUTTONDOWN_API_KEY is stored as an encrypted variable in the Worker settings.

When the worker receives a request it will first check that it's a POST request that was sent. If it wasn't a POST request it will reply with a 418 (because that's more fun than a 405).

If a POST request is receives, the Worker codes processes the data to extract the email that was sent. Once the email is extracted it generates a POST request to the Buttondown API to add the email that was provided as a new subscriber. Once the email is successfully added the Worker replies back with the response and the messaging on the newsletter form is update telling the user to check their inbox to confirm the subscription.

I referenced the index.js code on Cloudflare developer docs for my use case.

🔗Cloudflare Worker route

Instead of sending the POST request generated by app.js directly to the Cloudflare Worker, we can configure a route which means that this worker will execute when a URL is accessed. In my case, I configured the route to be https://sideofburritos.com/signup which means that when that URL receives a POST request with the correct data, the Worker code is executed and adds the provided email to the subscriber list.

🔗Scheduling Blog Posts

🔗Background

My site is hosted on Cloudflare Pages. The site content is on GitLab, and there's no way to schedule a merge from devmain which would trigger an automatic deployment. For a while I was just publishing my blog posts a day or two before my videos because I was assuming no one was looking at my site. Until someone posted a link to the post on Twitter a few hours before my video went live. Not that this was a bad thing, but I used it as an excuse to implement a way to schedule my posts related to my videos.

🔗Deploy hook with a Worker

little-dew

I used this post as a reference. Essentially what's happening is that using a worker to make a POST request to the deploy hook associated with the Pages site we want to update, it will trigger a deployment.

I created a new HTTP handler worker with the following code:

async function gatherResponse(response) {
  const { headers } = response;
  const contentType = headers.get('content-type') || '';
  if (contentType.includes('application/json')) {
    return JSON.stringify(await response.json());
  } else if (contentType.includes('application/text')) {
    return response.text();
  } else if (contentType.includes('text/html')) {
    return response.text();
  } else {
    return response.text();
  }
}

async function handleRequest() {
  const init = {
    method: 'POST',
    headers: {
      'content-type': 'application/json;charset=UTF-8',
    },
  };
  const response = await fetch(`${SOB_DEPLOY_HOOK}`, init);
  const results = await gatherResponse(response);
  return new Response(results, init);
}

addEventListener('scheduled', (event) => {
  event.waitUntil(handleRequest());
});

SOB_DEPLOY_HOOK is a variable configured for the worker that contains the deploy hook URL. Deploy hooks contain sensitive data, so they're better stored as an encrypted variable.

🔗Trigger setup

My videos are scheduled to publish every Monday at 14:00 UTC. I want this trigger to run before my video publishes so it's life, but not too close to the publish time where it doesn't have time to propagate. I schedule the cron trigger for the following 50 13 * * mon. Now, 10 minutes before my video is scheduled to publish my staged changes on my dev branch will be deployed.

🔗Remove Routes

As we don't need the route for the worker published, make sure to disable the default route that's created.

🔗Cache purge with a worker

🔗Background

At this time, Cloudflare pages doesn't have an automatic was to purge the cache for a zone when there is a new deployment. As a work around, I'll be using a worker to purge the cache for the entire zone 5 minutes after the deployment above occurs.

🔗Cache purger worker setup

weathered-rain

I created a new HTTP handler worker with the following code:

async function gatherResponse(response) {
    const { headers } = response;
    const contentType = headers.get('content-type') || '';
    if (contentType.includes('application/json')) {
      return JSON.stringify(await response.json());
    } else if (contentType.includes('application/text')) {
      return response.text();
    } else if (contentType.includes('text/html')) {
      return response.text();
    } else {
      return response.text();
    }
  }
  
async function handleRequest() {
  const init = {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'Authorization': `Bearer ${SOB_CACHE_PURGE_TOKEN}`,
    },
    body: '{"purge_everything":true}',
  };
  const response = await fetch(`https://api.cloudflare.com/client/v4/zones/${SOB_ZONE_ID}/purge_cache`, init);
  const results = await gatherResponse(response);
  return new Response(results, init);
}
  
addEventListener('scheduled', (event) => {
  event.waitUntil(handleRequest());
});

SOB_CACHE_PURGE_TOKEN is an encrypted variable for an API token that has permission to purge the cache for a specific zone.

SOB_ZONE_ID is an encrypted variable with the zone id of the zone we want to issue the cache purge for.

🔗Trigger setup

My videos are scheduled to publish every Monday at 14:00 UTC. I want this cache purge to trigger 5 minutes after the above deployment occurs. I schedule the cron trigger for the following 55 13 * * mon. Now, 5 minutes after my blog post is published, the cache for the zone will be purged and the content will be available for when my video is published.

🔗Remove Routes

As we don't need the route for the worker published, make sure to disable the default route that's created.