Project Updates, Harriet Tubman Day
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.
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.
Not Breaking the Search
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ā¦
ā¦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 © <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.
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ā¦
By commenting, you agree to follow the blog's Code of Conduct and that your comment is released under the same license as the rest of the blog.
Tags: programming project devjournal css jekyll