Ghost Assessment Plugin
So, to casually plug https://curatingcollections.com, I will mention that site is one that I also set up and maintain - yet write none of the content. That is my lovely wife Crystal, who you can find over there posting about her passion for history.
However, since she's also attempting to start a consulting business, we had to find a way to get her some qualified leads. One of the things I've always wanted to do was to have an assessment funnel. Who doesn't like to take online quizzes, right?
How to do a survey
So I ended up using SurveyJS + Ghost as a basis for doing just that. Of course, the backend was a bit trickier, but I'll get into that later.
To get into the overview, I actually have Ghost compiling on NixOS - and if I'm brave enough, I'll share my setup to that someday. However, most notably, as part of this, I have a GlusterFS share that cross-mounts all of Ghost's data. Which means, I can throw my theme on there, and edit it once, and have all of the servers serving it get the update.
Additionally, I have all of Ghost's native code injection and HTML card aspects to play around with in order to make this work.
HTML Cards
The first part of this was to put together the HTML cards into a working webpage. This looked like three sections:
Section 1: Importing Libraries
This was easy to do since they are hosted at https://unpkg.com. I would've otherwise liked to self-host them, but this'll do for now. I already use Ghost's hosted membership callback library, so it's not untoward to use this feature hosted as well:
<link href="https://unpkg.com/survey-core/survey-core.min.css" type="text/css" rel="stylesheet">
<script type="text/javascript" src="https://unpkg.com/survey-core/survey.core.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/survey-js-ui/survey-js-ui.min.js"></script>Section 2: CSS Styling
This was what I used AI for the most. I just kept iterating over and over until I got to a good place where I was happy with the results. The original branding was an ugly shade of turquoise, and the brand for the site is a type of burnt-orange, so that didn't jive at all. Protip - make sure to check that all the buttons and dials work when you interact with them if you go changing the CSS. They didn't all work for me, and that was a nasty surprise that I figured out luckily before everything went live.
Section 3: The Actual Survey
There's tons of instructions on how to set up a survey, however, there were a couple of neat things that I did in implementing this specifically.
The Backend
It's probably time to loop around to the backend, as that's what most of the features in the frontend are for, anyways.
I've got a tiny Flask application that forwards JSON POST requests as formatted HTML to my wife's email (through a postfix relay that authenticates to Fastmail as a smarthost SMTP proxy) so when someone hits "Submit" on the form, all the data is captured and sent to us via email. Is it foolproof? No, but it's functional, and that's all that's required in the here and now.
There are a couple of layers of protection that were implemented here:
- Honeypot
- There is a hidden field on the form that no human would ever fill out, but that is tempting to bots. If that field is not blank, then the email does not get sent, yet returns a 200 to the end-user, so that anyone trying to scrape the site will not know that they are getting detected and foiled.
- The thought behind this is that no user would ever fill this out, so only bots would submit a form with this field filled out. Therefore we disregard all traffic with this field filled out.
- Elapsed time
- There is another field which captures the time that the page loads, and the time that the form is submitted. If that time is under 5 seconds, then it will do the same as a secondary counter-measure.
- This is passed simply as an integer of milliseconds and we simply confirm that it's above 5000.
- Assessment Key
- Lastly, there is a hard-coded key that is present in the Javascript which, without it, would fail in that same silent manner.
- The thought behind this last one is that if one were to be blindly scraping the site, and not pausing to analyze the requirements of the endpoints, anything submitted to the endpoint without that as an additional parameter (which isn't immediately obvious, but a cursory read-through of the code reveals it) would also get rejected out of hand.
This is simply defending against the low-hanging fruit, nothing that would actually hold up to advanced spamming. However, being small enough to not be a target of anyone worth defending against has its advantages in that covering the 90% is good enough.
The Frontend
So now we get to the components of how this all comes together.
First, I put the actual SurveyJS code into an HTML card beneath the imports and the CSS. Then I created an assessment.hbs in the theme that I could import into the front page, that had a teaser link to the page where the code was all hosted. This way, we have a bold call to action for anyone looking at anything at the blog.
The Network
Lastly, I added another route, not in Ghost, but rather in my reverse proxy (details on my homelab network to follow later), but that would route to the flask app on the same domain. So that hitting the endpoint would not route to Ghost, but rather to the little Flask app that would template and forward the email.
The Result
The end result of this was a very nice little addendum to Ghost, which could (if I wanted it to) be completely self-hosted, and let me put together a very stylistically-pleasing looking intake assessment form on Ghost, without having to sign up for any of the other services like SurveyMonkey or Typeform or anything else.