Time Machine

For the last seven-odd years I’ve kept a running log of the songs I’m listening to at any given time.

It’s a simple routine I started with the late-great Rdio: create a new playlist at the beginning of each month—“May 2015”—and drag songs in as I recognize they’re on regular repeat.

It’s imperfect and lossy; undoubtedly, many songs have fallen through the cracks over the years. Still, I find the ritual of doing it by hand to be soothing, and it’s such a gift to travel through time for just a few minutes.

I’ve been seeking out comfort lately, as I’m sure a lot of people can relate, and finding it in a lot of music that makes me think of the past.

— Casey Kolderup, Amplifier

Especially now.

Perf’s Up

Last month, Zach Leatherman retooled the list of sites built with his static-site generator Eleventy, transforming what was originally a flat list into a leaderboard of sorts:

Sites with Lighthouse scores greater than or equal to 90 are ordered by performance. The remainder are ordered randomly. Performance rankings are updated approximately once per week on Sunday.

I have a single-serving site on the list, Eats & Drinks. It’s nothing special, just a list of places in Portland my wife and I enjoyed when we lived there. (We moved to Denver last fall, so the site is on permanent hiatus.)

As Zach noted shortly after launching:

I’ve already received multiple reports of people updating their sites to be even faster to try and break into the Top Eleven results.

I’m not a competitive person and Eats & Drinks is no longer in active development, but I started to feel bad watching my little site drop in the rankings week after week, so I dusted things off and got to work. Here’s what I’ve done so far to improve on the site’s original SpeedIndex median score of about 1887.

wave off Boomerang Beach, Australia
Photo by Holger Link

First Pass

I know very little about web performance strategies and have absolutely zero experience improving an actual site’s actual performance, but I do listen to ShopTalk Show! I knew my first order of business should be to move some things off the critical rendering path.

Because the site is only one page, I originally defined its CSS internally using a single <style> tag up in the <head> block. I also bring in two fonts in a few different weights using a third-party stylesheet from Typekit:

	<link rel="stylesheet" href="https://use.typekit.net/lsz1tsk.css">

	{% set css %}
		{% include "css/index.css" %}
		{% include "css/header.css" %}
		{% include "css/footer.css" %}
		{% include "css/location.css" %}
	{% endset %}
	<style>{{ css | safe }}</style>

I figured I could safely defer <footer> styling as well as font loading by moving them to the very end of <body>:

	<link rel="stylesheet" href="https://use.typekit.net/lsz1tsk.css">

	{% set css %}
		{% include "css/footer.css" %}
	{% endset %}
	<style>{{ css | safe }}</style>

There are tools to fine-tune and automatically identify which styles fall on the critical path, but this seemed like a good place to start. Did it help the following week’s ranking?

Performance Rank #74 ↑ 46

Sure did! Jumping solidly into the top 100 isn’t too shabby for a few lines of work.

Digging In

It turns out that development based on performance ranking is a virtuous cycle, and it’s kind of fun to boot. But my method of working (very) asynchronously—make a change, wait until Sunday for the results—felt a little ridiculous, so I started running my local development copy of Eats & Drinks through the Lighthouse CLI to find more areas for improvement.

Putting the "D.I.Y." in font-display

Lighthouse flagged an issue with text rendering:

Ensure text remains visible during webfont load

Unfortunately, Typekit’s @font-face definition explicitly specifies font-display: auto and they don’t provide any support for the new swap value that instructs supporting browsers to display text in a fallback font until the specified font is ready. (This results in brief period during rendering when no text is visible at all, which makes for a pretty janky experience.)

This felt like an insurmountable hurdle until I realized I could bypass Typekit’s entrypoint CSS and host my own version instead, with one important modification:

font-display: swap;

My original idea was to create a drop-in replacement for the Typekit-hosted CSS, swapping in a self-hosted external CSS file for theirs:

<link rel="stylesheet" href="/fonts.css">

But I had a hunch there was still a bit of room for improvement: any external CSS file, whether it’s mine or Typekit’s, still requires an additional network request. Since I’m now self-hosting the @font-face CSS, I can load it however I like:

	{% set css %}
		{% include "css/fonts.css" %}
		{% include "css/footer.css" %}
	{% endset %}
	<style>{{ css | safe }}</style>

Moving font definitions to the internal CSS alongside <footer> styling at build time comes in at a median SpeedIndex score of… 843 🔥 Hot damn!

I don’t know for sure how these changes will pan out in this week’s rankings, especially since plenty of other folks are busy tuning their sites as well, but I’m optimistic I’ll see at least another modest jump 🤞 And even if not, I learned a little bit about how to make sites more performant.

Time Travel with Netlify Deploys

Unfortunately, I didn’t grab Lighthouse scores for Eats & Drinks in its original, pre-improvement state. Lucklily, Eats & Drinks is hosted on Netlify! Thanks to their immutable deployments feature, it’s easy to quickly run Lighthouse against any point in the site’s history.

Just like we can pass in a production URL:

$ lighthouse https://pdx.ashur.cab/ \
	--chrome-flags="--headless" \
	--only-categories=performance \
	--quiet \
	--output=json | jq '.audits["speed-index"].numericValue'

we can also test against the deploy preview URL for any commit we want to investigate:

$ lighthouse https://5e7989ad1d9b3300088ccb67--mystifying-swirles-c1a0c9.netlify.com/ \
	--chrome-flags="--headless" \
	--only-categories=performance \
	--quiet \
	--output=json | jq '.audits["speed-index"].numericValue'

😗🤚 P.S. If you work with JSON on the command line even a little, do yourself a favor and add jq to your toolset.

GitLab Quick Actions

GitLab has become an invaluable part of how we do development and testing at Panic, spanning products, platforms, and disciplines. We use it to host our projects, track issues, organize milestones, and manage merge requests ("pull requests" in the GitHub nomenclature).

Issues and merge requests in particular are constantly in flux: assignment changes, labels come and go, milestones get assigned and reassigned. GitLab’s interface is great—things are where you’d expect them—but I’m always looking for ways to reduce friction and automate things I do all day long.

GitLab Quick Actions are a huge gift to folks like me who feel most productive using a keyboard. Any comment field doubles as a kind of context-specific command line:

Performing three quick actions on a merge request in GitLab

Throw in a quick Command-Return to submit & execute

It’s a small trick, but quickly performing a wide array of actions all without leaving the keyboard is a real game changer.


The underlying functionality deployed quietly a couple of weeks ago, but a new featurette I’m calling All-Stars will make its little debut on the home page today, just below this post.

Here’s how it came together.

silhouette of mountains in front of stars
Photo by Denis Degioanni


If my math is correct, I’ve been reading kottke.org for damn near half my life.

When the idea to build Multiline Comment started popping around my daydreams, I knew I wanted to borrow a small Kottke-ism straightaway: a Quick Links-like interstitial made up of articles and other links I’ve been reading around the web.

(I don’t flatter myself to think the two-penny opera I’m conducting here is comparable to what Jason has built over the last twenty-odd years. My insight doesn’t run deep enough to pull off a full-blown link blog, for one. Still, my internal recipe for what constitutes a blog is so deeply seasoned by two decades of following his work that I’m bound to cop a move or two, consciously or otherwise.)

Design & Planning

By virtue of building this site with Eleventy, posts like this one are created as individual Markdown files and then extruded through Nunjucks templates at build time into blog-shaped HTML. It’s a regular Mop Top Hair Shop up in here.

For one hot, stupid minute, I considered using a global JSON data file for logging and publishing links. (It passed quickly.) Creating and editing text files is perfect for writing whole blog posts, but it’s too much overhead for easily sharing a URL here and there, and anyway editing JSON by hand makes me grumpy.

I wanted the process to be easy enough for it to become sticky; realistically, if it was too cumbersome, or required more than a tap or click or two, I’d never get into a good rhythm. Fortunately, I didn’t have to look far for a proper solution.

Made of Star-Stuff

I’ll tell anyone who listens how much I love Feedbin, my RSS service of choice. I spend a good portion of my browsing hours checking in there to catch up on blogs, email newsletters (yup!), and other feed-backed goodness.

Like all good websites, Feedbin uses stars for faving. Read something you like? Hit the star and fave it. Easy peasy. They also expose an API, which includes a query for faved items. If you squint just right, you can see the glimmering of a little link roll

The basic functionality, then, should be this: faving something in Feedbin will make it appear in All-Stars (eventually). Let’s dig in!

Fetching Faves

One thing I love about Eleventy is its flexibility. I’ve tried a number of static-site generators in the past, only to get frustrated by how their visions for a site structure or layout don’t match mine. Eleventy is different: everything feels designed to give people a hand building whatever comes to mind. It’s a tool not a solution, in the best possible sense.

A particularly nice and powerful feature of Eleventy is its support for JavaScript data files, useful for generating—or fetching!—dynamic content at build time. A script can return static data:

module.exports = [

a function:

module.exports = function() {
  return [

or even an asynchronous function:

module.exports = function() {
  return new Promise((resolve, reject) => {

This is perfect for grabbing data from a live API, munging the results, and handing everything off to the template system for rendering, which is exactly how the back half of All-Stars is implemented.

The API-calling logic lives in one data file and doesn’t do anything too fancy:

  1. Ask Feedbin for all my starred entries
  2. Sort them by id, largest to smallest (a so-so proxy for "most recently faved")
  3. Grab the six most recent entries (a nice, round number for layout: 1×6, 2×3, or 3×2)
  4. Clean up the hostname for display purposes (i.e., strip any leading www.)
  5. Give everything back to Eleventy

That’s it!

You’ll notice my Feedbin credentials aren’t stored in the script or anywhere in the site repository. (I love you but we can’t share a login; this isn’t Netflix.) Instead, the script looks for two environment variables, FEEDBIN_EMAIL and FEEDBIN_PASSWORD. During development, they’re set in a .env file at the root of the project folder. In production, they’re defined in Netlify’s friendly deployment dashboard.

Fetching Fakes

Adding an external source to my build process introduces a new wrinkle to doing local development.

I use Eleventy’s live-reloading serve function while tinkering on the project, which means the site is built each time I save my work. This is handy quality-of-life tool, but I’m not always online while I’m working. Even if I were, I don’t really want to hit the Feedbin API every single time I make a small change.

I added a quick Are we in production, or explicitly testing the Feedbin API? check at the top of the data function:

const {starredEntries} = require( "./dev/allStars-entries" );

// ...

if( process.env.NODE_ENV !== "production" && !process.env.FEEDBIN_LIVE )
    console.warn( "Using development data for All-Stars" );
    return Promise.resolve( starredEntries );

If not, the function immediately resolves with a set of fake data objects. Development build times are nice and speedy without a roundtrip to the Feedbin endpoint, and I don’t have to worry about whether API requests will resolve.

(P.S. Ask me how I’m using Nova’s awesome Build & Run Tasks feature to make starting the development server, testing live Feedbin data, and other development details a breeze 💫)

Rendering Faves

I love a good, tidy organization system, and breaking discrete components out into standalone templates really pushes my buttons. The All-Stars component is short and sweet.

One thing that tripped me up for a bit was how to place the component where I wanted it. I’ve always liked the interstitial layout on the kottke.org home page—the most recent post comes first, followed by Quick Links, followed by everything else—and I had hoped to emulate that. I mistakenly looked through the Eleventy documentation at first for a tool to tackle inserting an unrelated template in the middle of a data collection, but this seems to be the purview of templating languages instead.

Nunjucks offers a loop.index property, giving the current iteration of a given for loop, which we can use to conditionally include the All-Stars template after the second article:

{%- for article in pagination.items -%}
    {% include "article.njk" %}

    {% if loop.index === 1 %}
        {% include "components/all-stars.njk" %}
    {% endif %}
{%- endfor -%}

Incidentally, this is what has kept All-Stars hidden until now 😉


Feedbin’s API returns a nice big chunk of metadata about each entry, that looks something like this:

    "id": 1682191545,
    "feed_id": 1379740,
    "title": "Peter Kafka @pkafka",
    "author": "Peter Kafka",
    "summary": "In 2009, the big magazine publishers built their own digital service so they wouldn't be cut out by Apple or Google. Now they're selling to Apple.",
    "content": "<div>Content</div>",
    "url": "https://twitter.com/fromedome/status/973315765393920000",
    "extracted_content_url": "https://extract.feedbin.com/parser/feedbin/9197b49979d10d5012130f8b456bd5bd040d3206?base64_url=aHR0cDovL3d3dy5jcmFpZ2tlcnN0aWVucy5jb20vMjAxNy8wMy8xMi9nZXR0aW5nLXN0YXJ0ZWQtd2l0aC1qc29uYi1pbi1wb3N0Z3Jlcy8=",
    "published": "2018-03-12T21:52:16.000000Z",
    "created_at": "2018-03-12T22:55:53.437304Z",
    "images": {
        "original_url": "http://www.macdrifter.com/uploads/2018/03/ScreenShot20180312_044129.jpg",
        "size_1": {
            "cdn_url": "https://images.feedbinusercontent.com/85996e1/85996e10ef95a3b96a914e67dfc08d5d3362c6e0.jpg",
            "width": 542,
            "height": 304

I thought it would be a nice touch to show images whenever they’re available, but not every blog post and article has an image associated with it. By pure coincidence, I was working on All-Stars and staring at a smattering of empty gray placeholders when a link from Waxy.org rolled through:

Generative Placeholders

I had faved it in Feedbin, forgotten(!), and then stumbled across it again at the most opportune moment. As a result, faved entries that dont’t bring their own images get a nice, procedurally generated placeholder courtesy of @fourtonfish.

Keeping It Fresh

That’s the long and short of the All-Stars implementation itself. However, it’s mostly for nothing if links are only updated when I push a new change to GitLab and trigger a build in Netlify. Who cares about a bunch of stale links I faved a few weeks or months ago?

Netlify gets me off the hook, so to speak, for having to worry about this at all. I generated a new Build Hook in the deployment dashboard that builds the master branch any time it’s called. I added the bog-standard cURL command:

curl -X POST -d {} https://api.netlify.com/build_hooks/<build_hook_id>

to a script on my trusty old DreamObjects droplet, and created a cron job to run that script every six hours:

0 */6 * * * $HOME/scripts/multiline.co/all-stars.sh

That means the random passerby is at most six hours away from finding pipin’ hot faves fresh out of the celestial oven.


I’m pretty happy with how it turned out, but the current implementation leaves a couple of little gaps:

  • I can only share from within Feedbin. This felt like enough to get the ball rolling, but I’m working on an enhancement that will let me continue to use Feebin faves and share links I find elsewhere.
  • I love the generative placeholders, but Glitch’s built-in sleep feature can make things a bit slow on wake. This is probably okay, given the relatively low traffic this little blog gets 😊
  • The design itself bears more than a passing resemblance to ad dumpsters like Taboola and Outbrain, which is basically mortifying. I’ll need to fiddle with things to make it look less skeezy.
  • I’m trying to get away from relying on a shared VPS. I haven’t found a good cron-as-a-Service replacement yet, but if you have one you like please let me know!


So that’s All-Stars. Thanks for the inspiration all these years, kottke.org. Keep on favin’ ★

Wheels Up

Howdy! My name is Ashur, and my pronouns are he/him/his. This [gestures unnecessarily] is my blog.

My goal here, broadly speaking, is to take the extra legroom afforded by a site of my own to think longer, slower thoughts than I do on Twitter. (They may not be better, mind you, but they could hardly be worse.)

What to Expect

I love building command-line tools, for work and for fun. (It’s quieter in the terminal, I think, than the buzzing, honking, unending chaos of an internet we’ve clogged with commerce and surveillance.)

I also tend a small stable of “art” bots in my spare time. They’re great at making me feel creative, if undeservedly so, and I like the idea they might bring a brief blip of joy to someone’s timeline.

I’ve had a few false starts, but a couple of years into futzing around with JavaScript in my spare time, I finally feel like I’m starting to get the hang of things. It feels pretty good.

All of which is to say: you can look forward to—or regret—following along as I fumble my way through the ups and downs of programming and re-learning web development.

Airplane in the clouds
Photo by 贝莉儿 DANIST

That’s it, I think? Maybe I’ll see you around 👋