Building a Squarespace integration with Node.js

Javascript integrations must be built carefully to avoid excessive rendering and network overhead. If well-built, they make it easy to get get complex functionality in a website, without being locked into the vendor (e.g. Squarespace).

I built a small application that uses a search engine to find articles you’ve written for other people, so you can update your writing portfolio if they disappear. This is essentially what you’d do manually to monitor this, and it gives you a list of articles you’ve written. For this article, I’m going to show how to embed this in a Squarespace site.

Here is a sample of the data we want to embed:

squarespace3

Squarespace is a subscription platform for people who want a blog and e-commerce tools, but don’t want the maintenance headaches of WordPress. The templates look pretty nice out of the box:

squarespace2

Squarespace does allow developer customizations but they focus on modifying the look and feel of the site, like changing page templates or CSS. You can customize a site by checking out the template with git, but for non-technical users this is a hassle.

Ideally we want to be able to write a piece of Javascript that can be droped into the page:
squarespace4

While this type of integration has a lot of power, it does break if a browser is configured to block Javascript.

Before we start building something, lets consider an instructive example, the Google Analytics tracking script:

var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-1570898-2']);
_gaq.push(['_gat._forceSSL']);
_gaq.push(['_trackPageview']);
 
(function () {
  var ga = document.createElement('script');
  ga.type = 'text/javascript';
  ga.async = true;
  ga.src = 
    ('https:' == document.location.protocol ? 
     'https://ssl' : 'http://www') + 
     '.google-analytics.com/ga.js';
  var s = document.getElementsByTagName('script')[0];
  s.parentNode.insertBefore(ga, s);
})();

Information about your account is set on global variables, then a script block is added to the page, which loads asynchronously. It accesses the script from different domains depending on whether you are on SSL or not. Once loaded, Google Analytics monitors for specific actions, and reports back to the server as operations it tracks occur.

When designing a Javascript integration the least amount of code should live on the page, as it will be nearly impossible to change once people are using it. It may even be valuable to plan on versioning the API.

The backend code we’re going to render is a simple ExpressJS script. I’ve chosen not to add CSS classes, so that this can inherit the styling of the site it is added to:

<ul>
<% for(var i = 0; i < alerts.length; i++) {%>
  <li><%= alerts[i].title %></li>
<% } %>
</ul>

We can then set up a simple Javascript script that can be injected into the page, like so:

<script src="//garysieling.com/squarespace/1/embed.js" async="" defer="defer"></script>

We set async and defer to prevent the script from blocking other rendering activities.

To handle SSL and non-ssl pages, I’ve chosen to use a protocol-less URL. This has few downsides1. I haven’t found any documentation on why you’d want to use two domains (SSL and non-SSL) like Google Analytics does, but I suspect that this makes configuring a load balancer easier.

It’s worth noting that older implementations of this type of script used document.write to add the script to the page, which blocked rendering.2

Note that because I put a user ID in the URL, this script won’t be cached across user accounts. There will also be separate copies cached for HTTP and HTTPs.

For the backend of this call, we set up a template in Express.js that can add our code to the page:

"use strict";
(function() {
  var template = '<template from above example>';
 
  var id = document.querySelector('&lt;%= selector %&gt;');
  document.querySelector(id).innerHTML = template; })();
});

This illustrates some of the challenges to this integration.

6d6f4bd5-6153-4875-ba61-0791e2e99fb3If you want to write the above code to use ES6, or have this minified, it should be done in advance and checked in. While there is middleware that can minifies code3 it incur add a performance penalty (realistically it is probably much worse than that of having the browser deal with some extra whitespace).

Squarespace has multiple configurable templates, and they use a UI framework (YUI3) that randomly names divs on the page. Because of this we can’t choose a reliable selector to allow adding widgets to the page.

To make this integration robust, we need a way to configure where the widget content is placed. This can be done by allowing creating a configuration setting for the CSS selector, which sets where their portfolio will be visible on the page.

Compare this to the Google Analytics script, which sets global variables on the window object- this clutters up the namespace, but allows for better caching.

Speaking of caching, to make this work reliably, if we use settings, we need to be able to cache the output effectively. One option is to cache for a short period (e.g. 30 seconds)4, or to allow for a ‘developer mode’ that sets this cache header to 0. A more robust implementation would use ETags5. Here is a simple example:

app.get('/squarespace/:user_id/embed.js', (req, res) =&gt; {
  res.header("Content-Type", "application/javascript");
  res.header("Access-Control-Allow-Origin", "*");
  res.header("cache-control", "public, max-age=30, must-revalidate");
});

Passing a CSS selector to the script allows adding support for other platforms later and makes it easy to handle different themes. Alternately, the selector could also be set on the script tag itself as a data attribute:

<script 
  src="//aaa.herokuapp.com/embed.js" 
  async defer
  type="text/javascript" 
  data-name="6d6f4bd5-6153-4875-ba61-0791e2e99fb3" 
  data-id="div[data-type=&quot;page&quot;] div div div">
</script>

And then the actual Javascript to do the lookup becomes:

var id = 
  document.querySelector(
    'script[data-name="profile-script-6d6f4bd5-6153-4875-ba61-0791e2e99fb3"]'
  ).getAttribute("data-id");
 
document.querySelector(id).innerHTML = template;

The advantage of this approach is that it lets you configure the integration through the Squarespace UI, rather than opening two tabs. On the other hand, having integration parameters in your own application allows you to configure this for the client using the application, which makes support very simple.

By using a GUID, we can be reasonably sure that this won’t conflict with other scripts, but this does incur the performance penalty of using two DOM lookups instead of one.

Squarespace does not have a way that I can find to make Javascript only render on specific pages. Consequently, we also need to provide an option for users to filter which pages they want this change applied to (e.g. since this is a tool to display your portfolio, we might restrict it to “/about”).

For this application, I’ve allowed people to select specific pages to filter where the integration is applied.

We can add a piece of code that checks this:

if (window.location.pathname !== '/about') {
  ...
}

Alternately, if this was not available, we could use a regex to match the URL (this is hard coded to about):

(http|https):\/\/([^\/]*)\/\b(about)\b(\/)?([#?]|$)

If you find this interesting, there is a Manning Book called Third Party Javascript, which may be helpful as well (I haven’t read it yet, but hope to soon).

And there you have it! If you’re interested in monitoring a writing portfolio, email me at gary@garysieling.com to get early access to this tool.

Citations:
  1. http://blog.httpwatch.com/2010/02/10/using-protocol-relative-urls-to-switch-between-http-and-https/ []
  2. http://www.stevesouders.com/blog/2010/02/10/5b-document-write-scripts-block-in-firefox/ []
  3. https://www.npmjs.com/package/express-minify []
  4. http://www.mobify.com/blog/beginners-guide-to-http-cache-headers/ []
  5. https://en.wikipedia.org/wiki/HTTP_ETag
    []
0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *