JavaScript Mapping Library
Don’t do this. I’m serious. Or do it, I certainly don’t listen to reason when it comes to building demos. I’ve been in web development for a very, very long time, and I’ve seen many trends come and go. Guestbooks were a way for folks to leave a comment on your site as a whole. I haven’t seen one in ages, but some still linger. In fact, Ana Rodrigues has an absolutely lovely guestbook driven by Webmentions. And if you really want to, you can still download a Perl CGI guestbook over at Matt’s Script Archive. I haven’t written Perl in decades, but I absolutely loved it back in the 90s. That being said, I had a free hour yesterday, was bored, and decided, why not do something fun? The result – my new guestbook that you can visit today. Here’s how you too can (but don’t) add a guestbook to your Jamstack site. (My example is in Eleventy, but uses nothing specific to Eleventy.)
For my data, I decided to store information in a Google Sheet. That’s a pretty lame database, but it worked easily enough. I set up a Google Sheet with four columns: Name, Comment, Date, and Approved. Name and Comment should be self-evident, but Date is a ‘time since epoch’ numerical value and Approved is TRUE or FALSE.
TRUE
FALSE
To retrieve guestbook entries, I built a Pipedream workflow with the following steps:
A2:D10000
export default defineComponent({ async run({ steps, $ }) { return steps.get_values_in_range.$ return_value.filter(a => a[3] === 'TRUE').map(a => { return { name: a[0], comment: a[1], date: a[2] } }); },})
export default defineComponent({ async run({ steps, $ }) { await $ .respond({ status: 200, headers: { "Content-Type":"application/json"}, body: JSON.stringify(steps.filter_and_map.$ return_value), }) },})
You can see the result of this yourself here: https://eoxzk4xd3lr6trv.m.pipedream.net/.
To add a guestbook entry, I created another Pipedream workflow. It’s also HTTP triggered of course, and does the following:
Edited at 1:47PM I just added a new step to validate that the name and comment field was sent. Duh, I should have done that initially. If they are not passed, the workflow ends.
First, it uses another built-in Pipedream action that adds rows to Google Sheets. I look for the name and comment value in the body of the HTTP trigger, set Date automatically, and Approved to false. For the most part, this just worked, but check out what I do with Name and Comment:
For both of the user-submitted content, I strip out any and all HTML. This is a safety measure to ensure nothing naughty gets in. I’ve already set it up such that it’s set to not being approved by default, but this extra step ensures I don’t have to manually clean input.
export default defineComponent({ async run({ steps, $ }) { let html = `<h2>Guestbook Entry Submission</h2><p>The following information was submitted:</p><p>Name: <b>$ {steps.trigger.event.body.name.replace(/<.*?>/g,'')}</b></p><p>Comments:<br/><b>$ {steps.trigger.event.body.comment.replace(/<.*?>/g,'')}</b></p><p><a href="$ {process.env.APPROVE_GB}/?range=$ {encodeURIComponent(steps.add_single_row.$ return_value.updatedRange)}">Click to Approve</a></p> `; return html; },})
The beginning just outputs the simple values. For approval, I’m referencing the third and final workflow I’ll show next. I used an environment variable for that because I want to keep the URL secret, and my Pipedream workflow is tied to a public GitHub repository so I can share stuff. Obviously, that was my choice and I could have used a private repo. To handle knowing what to approve, I used the result of the previous step that added the data. The updatedRange value looks like so: Sheet1!A9:D9.
updatedRange
Sheet1!A9:D9
The third and final Pipedream workflow is another HTTP-driven workflow with just two steps (ignoring the trigger):
{{steps.trigger.event.query.range.split(':').pop()}}
export default defineComponent({ async run({ steps, $ }) { await $ .respond({ status: 302, headers: { 'Location':'https://www.raymondcamden.com/guestbook' }, }) },})
Ok, so far I’ve shown the serverless functions built on Pipedream to support the workflow. Now let’s look at how it’s rendered. You can visit the guestbook now, but if you’d rather not open a tab, here it is in all its glory:
Given that we’re building on the Jamstack, we’ve got multiple different ways to build this.
data
I went with option two for simplicity’s sake. Here’s the HTML I used:
<div id="entries"> <i>Loading guestbook entries, please stand by. It will be worth the wait, honest.</i></div><h3>Add Your Entry</h3><form id="addEntry"><p><label for="name">Your Name:</label> <input id="name"></p><p><textarea id="comment" placeholder="Your comment"></textarea></p><p><input type="submit" value="Save Entry" id="submitButton"></p></form>
Fairly simple as the real work is done in JavaScript. Here’s the entirety of that:
const GB_URL = 'https://eoxzk4xd3lr6trv.m.pipedream.net';const ADD_GB = 'https://eo8ymuvqefph1ce.m.pipedream.net';const formatter = new Intl.DateTimeFormat('en-us', { dateStyle:'full', timeStyle:'short'});let $ name, $ comment, $ button, origTextButtonText;document.addEventListener('DOMContentLoaded', init, false);async function init() { console.log('fetching gb entries'); let entryReq = await fetch(GB_URL); let entries = await entryReq.json(); let s = ''; entries.forEach(e => { s += `<div class="entry"><b>$ {e.name}</b> said on $ {formatter.format(e.date)}:<br/><i>$ {e.comment}</i></div> `; }); document.querySelector('#entries').innerHTML = s; console.log(entries); $ name = document.querySelector('#name'); $ comment = document.querySelector('#comment'); $ button = document.querySelector('#submitButton'); origTextButtonText = $ button.value; document.querySelector('form#addEntry').addEventListener('submit', addEntry, false);}async function addEntry(e) { e.preventDefault(); let name = $ name.value.trim(); let comment = $ comment.value.trim(); if(name === '' || comment === '') return; console.log(name, comment); $ button.setAttribute('disabled', 'disabled'); $ button.value = 'Submitting...'; let resp = await fetch(ADD_GB, { method: 'POST', body: JSON.stringify({ name, comment }) }); // currently we assume 204 and don't handle errors, maybe later... $ name.value = ''; $ comment.value = ''; $ button.removeAttribute('disabled'); $ button.value = origTextButtonText; alert('Your guestbook entry is submitted for approval. Thank you!');}
From the top, I initialize a few variables, and on document load, hit my endpoint to get entries. I render them and use the browser’s Intl API to nicely render the dates and times. The form submission is also pretty vanilla – basically, just ensure the name and comment aren’t blank, I don’t even tell the user if they made a mistake.
Intl
If you want to do this yourself, you can find the code here:
Come back next week when I’ll show you how to add a page counter to your site!
Raymond Camden
You must be logged in to post a comment.
This site uses Akismet to reduce spam. Learn how your comment data is processed.