Harriet Tubman

As mentioned last week, now that I have Bicker mostly operational, Iā€™m going to reduce the cadence of working on that code in order to make room for other projects. But, I think the ā€œdeveloper journalā€ has been useful, so Iā€™ll continue on with the Monday morning posts.

Blog Updates

I dumped an assortment of upgrades to the blog code, this past week. It was well past time for another upgrade and Jekyll is still accomodating.

Suggestions

Probably the biggest change comes from realizing that I now had released around sixty posts and am starting to see increasing trafficā€”welcome, readers from Albania, Argentina, Australia, Austria, Belgium, Brazil, Canada, China, Denmark, France, Germany, Hungary, India, Ireland, Israel, Italy, Nepal, the Netherlands, New Zealand, Norway, Panama, Romania, Russia, Serbia, Spain, Switzerland, Thailand, Turkey, the United Kingdom, and the United Statesā€”come through the blog. While those are definitely milestones that please me, it also means that the blog has the potential to look intimidating to a new reader. That is, if a potential reader drops into the blog index, thereā€™s no real way to get a sense of where to start reading.

The solution? Recommend some posts!

I now mark certain postsā€”the writing that might be of broader interest or that explains a longer-term projectā€”with a recommendation blurb that appears in a sort of slide-out tray on index pages.

Open Tray

The links are collected by an included component that tracks down and formats those recommended posts. Be warned that putting Liquid template code into a Jekyll post gets processed the wrong way around, so Iā€™ve replaced the braces ({}) with square-brackets ([]) to prevent the code from actually including the recommended URLs here instead of showing you the code.

<ul>
  [% for post in site.posts %]
    [% if post.recommend %]
      <li>
        <a href=[[ post.url | prepend: "/blog" ]]>
          [[ post.title ]]
        </a>
        <i>
          [[ post.recommend ]]
        </i>
      </li>
    [% endif %]
  [% endfor %]
</ul>

Maybe unsurprisingly, the heavy lifting is done in CSS, only opening the recommendations when the user hovers over the small tab. The following is simplified, but you can see the full file in the code repository.

.tray-panel {
  height: 40vh;
  left: 0;
  overflow-x: hidden;
  overflow-y: auto;
  position: fixed;
  top: calc(50% - 20vh);
  width: 2em;
}
.tray-panel .tray-pull {
  display: block;
  margin-top: calc(20vh - 0.5em);
}
.tray-panel .tray-contents {
  display: none;
  padding-left: 2em;
  padding-right: 2em;
}
.tray-panel:hover {
  width: 33vw;
}
.tray-panel:hover .tray-contents {
  animation: fadein 2s;
  display: block;
}
@keyframes fadein {
    from { opacity: 0; }
    to   { opacity: 1; }
}

The overflow properties prevent a rogue header from sticking out, while the position and top allow me to keep the panel fixed near the vertical center of the page, similarly with the tray-pull class centering the chevron in the panel. The hover pseudo-classes then change the width of the panel while hiding the chevron and revealing the real contents.

Note that my original vision was to use the header as a cue to open the panel instead of a chevron. In that case, I had code to rotate the header and move it into place as the text faded in. Eventually, I abandoned that idea as too difficult to center in the collapsed panel.

One problem with a blog that includes development and goes through some data format conversions is that backslashes (\) donā€™t always turn out well. The big impact I felt was on the search page.

The current approach to searching is to compile the entire blog into a single JSON document, which the search page uses to instantly find what you ask for as you type. Normally, thatā€™s fine, but a rogue backslash causes a lot of parsers to interpret the JSON as invalid.

I tried manually escaping all the backslashes (\\) in the Markdown files, but it didnā€™t always help and most of the doubled backslashes appeared in the final HTML.

Eventually, I worked my way through a few experiments with Liquid templates and determined that I could escape the backslashes just when creating the JSON files by calling replace: "\", "\\\\" on my post bodies.

ā€¦And then I needed to go through all the old posts with backslashes to fix them.

General Housekeeping

You might notice that Iā€™ve tweaked some other things with mostly cosmetic consequences. For example, the tags are now formatted more like buttons than code snippets and the tag page no longer has the unnecessary extra separator line at the bottom.

I also improved the handling of colors, to better fit whatā€™s possible in SASS.

Incidentally, something I donā€™t like about working with Jekyll that I donā€™t think Iā€™ve mentioned is thatā€”as far as Iā€™ve been able to tellā€”most of my CSS changes need to be marked as !important to override the existing themeā€™s style. That seems like a recipe for disaster. Another small issue is that, since Liquid fills in its templates before anything else happens, I canā€™t easily show any Liquid code. Other than that, this hasnā€™t been bad at all!

Small Upgrades

I havenā€™t been paying it much attention, but Bicker needs upgrades to both the puma and nokogiri libraries. Conveniently, Dependabot monitors these things and creates pull requests, so there wasnā€™t much to be done other than accepting the request.

Character Generator

When I wrote about Seeking Refuge, I mentioned a tool that Iā€™ve been working on occasionally, to create the skeleton of a character outline for a global sample set.

One of the things Iā€™ve never liked about the project is that itā€™s just a script. I can open a terminal window and run itā€¦

Example output

ā€¦but thatā€™s deeply unsatisfying. I need to be in a position to run Node programs and canā€™t just point people to a running example, when they need it. So, Iā€™ve been thinking for a while that this might be better suited as a simple website. A presentation like that would also make it easier to include other media, beyond a tiny color swatch and a flag.

Oh, speaking of flags, the flag emoji doesnā€™t work correctly in most consoles. Rather than šŸ‡¹-šŸ‡·ā€‹, read it as šŸ‡¹šŸ‡·ā€‹ā€‹ā€‹ā€‹ā€‹. Yes, the flags are made up by the ISO country code emoji pair. Likewise, the hand emoji changes color based on the skin tone that follows it instead of separating them.

Since this is just displaying static or generated data, it looks like the best bet for a project like this is going to be Express. From a quick glance, it looks very much like a bare minimum framework that expects the developer to deal with everything. But again, weā€™re only talking about serving one page with data pieced together from files, so thatā€™s probably not a bad thing.

Once Express has been installed in a Node project (npm install --save express), itā€™s straightforward to get a single page up and running.

const bodyParser = require('body-parser');
const express = require('express');
const app = express();

app.use(express.static('public'));
app.use(bodyParser.urlencoded({ extended: true }));
app.set('view engine', 'ejs');

app.get('/', function (req, res) {
  res.render('index', { 'key': 'value' });
});

app.listen(5000, function () {
  console.log('Example app listening on port 5000!');
});

The app.get() function handles an HTTP GET request (and thereā€™s a .post() to go with it), whereā€”in this caseā€”we render a .ejs file, or JavaScript embedded in HTML. Thatā€™s pretty close to ideal, since we basically just want to generate a data payload and display it formatted cleanly as HTML.

I already have the code that generates the payload, albeit as text instead of something sensible like JSON, but thatā€™s an easy enough fix.

Then, the aforementioned .ejs files are just HTML templates with some JavaScript added in special tags. So, <%= key %> digs into the javascript object passed to res.render() above, and will (in this case) insert the word ā€œvalueā€ into the web page.

Some fonts (Alfa Slab One and Lora, snagged from Google Fonts), a decent color scheme, and a hacked-together pseudo-woodgrain background image (noise, motion blur) later, and itā€™s more or less what I wanted.

But Where Are We?

One of the reasons Iā€™ve wanted this project as a web page is one specific enhancement that would obviously never work in text: A map, showing the random location and the five nearest cities.

To do that, I donā€™t feel like using any of the big mapping services for a variety of reasons, licensing among them. So instead, I followed Leafletā€™s Quick Start Guide and had fully functional map (markers for the position and five cities with pop-up labels, panning, and zooming) inā€¦maybe half an hour?

Iā€™ll be honest, one of the things that finally convinced me to make this a standalone project was hearing someone talk about Leaflet and wanting to see how it is to use in a real project. It turns out that itā€™s a tiny bit clumsy, in that it uses JavaScript to transform an HTML element into the map (not uncommon), butā€¦other than that, the code looks something like the following.

var map = L
  .map('map')
  .setView([<%= person.coordinates %>], 8);
L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
  attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery Ā© <a href="https://www.mapbox.com/">Mapbox</a>',
  maxZoom: 18,
  id: 'mapbox/streets-v11',
  tileSize: 512,
  zoomOffset: -1,
  accessToken: '<%= mapboxToken %>'
}).addTo(map);
L.marker([<%= person.coordinates %>])
  .addTo(map)
  .bindPopup('<%=person.latitude%> <%=person.longitude%>')
  .openPopup();
for (var i=0; i<cities.length; i++) {
  var city = cities[i];
  var circle = L.circle([city.latitude, city.longitude], {
    color: '#b780a0',
    fillColor: '#95557a',
    fillOpacity: 0.5,
    radius: 10000,
  }).addTo(map);
  circle.bindPopup(city.name);
}

See? Create the map with coordinates and zoom-level. Set the map to pull the tiles from Mapbox; youā€™ll need your own account and token, but that took seconds and could replace it with another service, provided youā€™re willing to change that URL template. Add the marker and add 10km-radius circles to mark each city. It reminds me a bit of D3, where most work operating on an element can be done through a chain of method calls.

Example

Itā€™s not perfect, mind you. As you can see in the sample above, labels on the map, such as route numbers, frequently get cut off where the tiles change. But for a free library that moves this fast and smoothly? Itā€™s fine and I donā€™t need much more than that andā€”to be clearā€”I donā€™t really know if that label problem is in Leaflet, Mapbox, or some interaction between the two.

However, there are two changes that Iā€™ll eventually want.

  • Possibly hiding (or shrinking) the map on page load, with a way to expand it to full size, which has nothing to do with Leaflet.
  • Scale the map so that the six markers are all visible.

But for now, this definitely fits my requirements. The first one is just a little bit CSS and JavaScript and the second is almost certainly a ā€œget the bounds of a list of markers, and set the scale based on thatā€ kind of deal, so Iā€™m not concerned. Iā€™ll probably take care of at least one of the two in the upcoming week.

Serving the Application

The last big step is setting this up so that I can get at it through my existing web server. Unfortunately, this is non-trivial.

  • Express.js is running its own web server, so I canā€™t just dump the files onto my existing server, which is served by Apache.
  • I donā€™t see any evidence of a Node.js web framework that Apache will just run, like it will a Ruby or PHP application.
  • In theory, I could run the web server on a separate port and just point everybody to :5000 or whatnot, but Apache server is configured to automatically rewrite all HTTP requests to HTTPS and this application doesnā€™t do HTTPS.
  • The right way to handle this, then, is to use Apache as a proxy, asking it to act like a conduit between the browser and the ā€œrealā€ web server. However, I followed four separate sets of instructions on how to do this and couldnā€™t get it to work.
  • I should be able to ā€œcheatā€ and have Express.js use a self-signed certificate. That works for my own computer, but not for my server, since the rules are presumably different.
  • Finally, I can use Letā€™s Encrypt for a certificate, butā€¦well, once again, thatā€™s non-trivial.

As mentioned, for localhost, we can just self-sign a certificate.

mkdir keys
cd keys
openssl req -nodes -new -x509 -keyout key.pem -out cert.pem

But for a server, we need to be more clever. Be very careful, here. You can re-use an existing domain without any trouble. But if you use a domain that Letā€™s Encrypt uses as the name of the certificateā€™s files, you might be in for some confusion. Of course, if you only have one domain, feel free to use that certificate, since thatā€™s all you need.

certbot --webroot -w ./static -d domain.tld
# replace 'domain.tld' with the real domain, like
# I put the server on 'colagioia.net'
ln -s /path/to/domain/keys keys

On my server, the keys are stored at /etc/letsencrypt/live/domain.tld/. I also needed to mess around with permissions to make the link work. But otherwise, it was just a matter of changing the .listen() call with this.

var https = require('https');
/* ... */
app.use(require('helmet')());
/* ... */
https
  .createServer({
    key: fs.readFileSync('keys/key.pem'),
    cert: fs.readFileSync('keys/cert.pem')
  }, app)
  .listen(5000, function () {
    console.log('Example app listening on port 5000!');
  });

So far, so good, though I wasted far too much time working this out and would still much rather have the proxy. That should probably be in a try/catch so that people who arenā€™t interested in setting up the keys can function on HTTP.

To the Background!

Obviously, we donā€™t want to need me to log in to run the server for this to be usable, so instead, weā€™ll use the forever package.

npm install forever -g
forever start index.js

Andā€¦weā€™re live! šŸ„‚ Check it out at https://colagioia.net:5000/. I even set up a job that should (hopefully šŸ¤ž) restart the server on reboot, so this should be mostly hands-off. Well, hands-off, except that I still need to set up the aforementioned job to renew the certificate, so if youā€™re looking at this in forty-five days or so and you canā€™t access the serverā€¦thatā€™s going to be why.

Next

I have a couple of items to deal with for the background generator:

  • The renewal jobā€¦which I only just realized (seeing the status e-mail) is already handled for my normal domains āœ”ļø,
  • Scaling the map appropriately, and
  • Hiding the mapā€¦which is less likely, as Iā€™m less interested in that as time goes on and Iā€™ve gotten used to seeing it, honestly, soā€¦āœ”ļø.
  • A custom footer might be nice, so that people who host it can note whatever they need to note, like Iā€™m crediting Colagioia Industries for hosting manually.

So, the list pares itself down to scaling the map and the footer.

Iā€™d also like to write more tests for Bicker, this week, while the idea is still fresh. Thereā€™s also another project or two waiting, but Iā€™m not convinced the user interface technologies I want to use are up to the task.


Credits: The header image is Matte collodion print of Harriet Tubman, long since in the public domain, but appropriate, given that tomorrow is Harriet Tubman Day and the Trump administration has punted on putting Tubman on the twenty-dollar billā€¦