Website - 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 layouts
β partials
β sidebar
β newsletter.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 layouts
β partials
β blog
β single.html
. This is the layout that will then be used when creating a new blog post.
{{ define "main" }}
{{ .Title }}{{ with .Params.emoji -}} {{ . | emojify }}{{ end -}}
{{ partial "main/blog-meta.html" . }}
{{ .Params.lead | safeHTML }}
{{ .Content }}
{{ end }}
{{ define "sidebar-footer" }}
{{ partial "sidebar/newsletter.html" . -}}
{{ 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 }}
.
{{ partial "sidebar/newsletter.html" . -}}
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 - assets
β scss
β common
.
The first file we're going to add is app.scss
in assets
β scss
. 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 assets
β scss
β common
.
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 assets
β js
β app.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 = 'Nice. Check your inbox for a confirmation e-mail.
';
} else {
form.innerHTML = 'Error: If you didn\'t expect an error, please let me know.
';
}
})
}
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 dev
β main
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.