All-Stars

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

Inspiration

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 = [
  "user1",
  "user2"
];

a function:

module.exports = function() {
  return [
    "user1",
    "user2"
  ];
};

or even an asynchronous function:

module.exports = function() {
  return new Promise((resolve, reject) => {
    resolve([
      "user1",
      "user2"
    ]);
  });
};

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 );
}
src/_data/allStars.js

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 -%}
src/_includes/layouts/index.njk

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

Images

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
glitch.me

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.

Leftovers

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!

Epilogue

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