Generating Automatic ShortURLs with NewBase60

This article, part of the writing collection, was published on .

I’m still auto-generating ShortURLs for each of my posts using Tantek Çelik’s NewBase60, but in a much more organised fashion and leaning into the JavaScript-based ecosystem of Eleventy.

In 2019, I wrote about generating unique ShortURLs for each of my blog posts using just Liquid templating. Back when we were beholden to just 140 characters to express ourselves, and before big silos took it upon themselves to generate their own ShortURLs, including a link on a social media post meant less room for your own thoughts. This catalysed an eruption of URL-shortening services, often putting an ad between visitors and their destination. Since I had discovered the IndieWeb, felt at home in its ecosystem, and despised the idea of putting barriers around my content—let alone someone else’s barriers—this motivated me to build my own.

I’ve moved over to Mastodon where the character-limit is 500 and better tooling diminish the usefulness of ShortURLs. Regardless, I like having them, and since my website is also now built using Eleventy, that change brought with it the ability to generate the same ShortURLs with some simpler JavaScript logic. Furthermore, an open source implementation of JavaScript-based NewBase60 already exists, so the work of getting it integrated into Eleventy is already started for us.

In this article, I’ll walk through how I implemented this in the latest iteration of my website. Of course, while I personally use Nunjucks instead of Liquid for my website, you can always continue to use the Liquid method if that’s your preference, as Eleventy supports both out of the box.

Wait, where are we?

Let’s refresh by taking a look at an example ShortURL and what each of its constituent parts represents:

repc.co/a4zK2
  1. aCategory Code (required, 1 character)
  2. 4zKSexagesimal Epoch Days (required, 3 characters)
  3. 2Post Index for the Day (optional, 1 character, default = 1)

These three segments allow us to uniquely identify any given post by referencing:

  1. Which category the post belongs to (article, note, bookmark, etc.)
    Note: this implementation assumes each post is assigned to a single category; tagging is used to group posts into multiple collections
  2. The post’s published date (year, month, and day)
    Note: this implementation assumes each post has a unique date
  3. If there are other posts in the same category with the same published date, the post’s published time is compared against those posts to chronologically determine if the post is the 1st, 2nd, 3rd, etc. for the day

Category Code

repc.co/a4zK2

One way to attach the category code to each post is to use Front Matter:

---
title: My First Article
category: article
categoryCode: a
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

However, I think a cleaner way is to use Eleventy’s Template and Directory Specific Data Files. We can make things easier by organising our posts into subfolders based on their category. Something like this:

posts/
├ posts.11tydata.js
├ articles/
│ ├ articles.11tydata.js
│ └ my-first-article.md
├ notes/
│ ├ notes.11tydata.js
│ └ my-first-note.md
…

By organising our posts in this way, we can attach metadata to every post within a subfolder/category without filling our Markdown files’ Front Matter with repeated metadata.

For every post in the articles folder, we can define some default metadata in articles.11tydata.js:

module.exports = {
	permalink: "article/{{ page.fileSlug }}/",
    tags: ["article"], // this creates an Article Collection
	category: "article",
	categoryCode: "a",
}

Likewise, for every post in the notes folder, we can define some default metadata in notes.11tydata.js:

module.exports = {
	permalink: "note/{{ page.fileSlug }}/",
    tags: ["note"], // this creates a Note Collection
	category: "note",
	categoryCode: "n",
}

We can also define some default metadata for every post in the posts folder, including subfolders like articles and notes above, in posts.11tydata.js:

module.exports = {
    tags: ["post"], // this creates a Post Collection
}

Sexagesimal Epoch Days

repc.co/a4zK2

Compared to the previous Liquid solution, things are quite a bit more terse in Eleventy where we can essentially implement NewBase60 as provided by Tantek Çelik.

However, that doesn’t mean this is simple by any means. I won’t even try to explain it, but it seems that humans—whether through nature, nurture, or something else—have an inherent preference for a base-10, or Decimal numeral system. So when it comes to even understanding how numbers represented in a different system relate to our familiar Decimal system, it certainly doesn’t come easily.

I would guess that the most widely-understood numeral system that is not our own (Decimal) is probably the binary numeral system, or the language of computers some might say.

Things get quite a bit more tricky beyond that. In the case of what we’re building, we’ll be using a Sexagesimal, or base-60 numeral system. While difficult to translate to Decimal in your head, this numeral system isn’t completely foreign to us.

It originated with the ancient Sumerians in the 3rd millennium BC, was passed down to the ancient Babylonians, and is still used—in a modified form—for measuring time, angles, and geographic coordinates.

Sexagesimal on Wikipedia

One immensely-useful aspect about the number 60 is that it divides nicely into 1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, and 60 (it has twelve factors). It’s a bit of a shame that a 60-column grid system didn’t gain traction back when projects like the 960 Grid System and its ilk were extremely popular!


Let’s get building.

First, we need to decide how we want to represent a base-60 numeral system. Fortunately for us, Tantek has already thought this out and came up with the following sequence. I suggest reading Tantek’s explanation as to why these particular characters were chosen.

// 60 characters that make up our Sexagesimal numeral system
const SEQUENCE = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz"

Now we can just about drop Tantek’s JavaScript implemention of NewBase60 into our project.

The purpose of this code is to consume a JavaScript Date Object and return the Sexagesimal value for how many days since Epoch, 01 January 1970 00:00:00 UTC, the given date is:

// Converts a Decimal (Base 10) Integer to a Sexagesimal (Base 60) String
const DecimalToSexagesimal = (value) => {
	if (value === undefined || value === 0) {
		return 0
	}
	let sexagesimalValue = ""
	while (value > 0) {
		let index = value % 60
		sexagesimalValue = SEQUENCE[index] + sexagesimalValue
		value = (value - index) / 60
	}
	return sexagesimalValue
}

// Converts a JS Date Object to a Sexageismal (Base 60) String
const DateToSexagesimal = (dateObject) => {
	let sinceEpoch = dateObject.getTime()
	let epochDays = Math.floor(sinceEpoch / (1000 * 60 * 60 * 24))
	return DecimalToSexagesimal(epochDays)
}
Example Conversions
Decimal Binary Sexagesimal
0 0 0
1 1 1
2 10 2
10 1010 A
59 111011 z
60 111100 10
901 1110000101 F1
9001 10001100101001 2W1
90001 10101111110010001 R01

NewBase60 Calculator

I’ve built a calculator to convert between decimal (base 10), binary (base 2), sexagesimal (base 60), and even the date, based on the number of days since Epoch.

Post Index for the Day

repc.co/a4zK2

The last step in building the ShortURL is to pull everything together into a function that we can pass to Eleventy to use as a Filter:

// Export a function for an Eleventy Filter
module.exports = (date, categoryCode, collection) => {
	// Get all posts where DATE matches in UTC
	const postsToday = collection.filter((post) => {
		if ("date" in post.data) {
			return new Date(post.data.date).toLocaleDateString("en", { timeZone: "UTC" }) === new Date(date).toLocaleDateString("en", { timeZone: "UTC" })
		}
		return false
	})

	// Get the index of the post where EPOCH TIMESTAMP matches
	// Note: Indices start at 1 for ShortURLs
	const postIndex = 1 + postsToday.findIndex((post) => {
		return new Date(post.data.date).getTime() === new Date(date).getTime()
	})

	// Build the string
	return categoryCode + DateToSexagesimal(date) + postIndex
}

Predictably, the function accepts three parameters, the same ones that we outlined above, and spits out a unique string identifier. The Post Index for the Day is determined at this stage, where we have access to the Collections objects in Eleventy. This is done by building an array of posts made on the same day, ordered chronologically, and selecting the index of our respective post out of that array.

Generating redirect pages

At this point, Eleventy still doesn’t know what to do if a user hits our site requesting a ShortURL, so let’s address that by creating a new file somewhere in Eleventy’s input directory. In this page, we’ll use Pagination to get Eleventy to generate a complementary redirect page for every content page in the Post collection:

Liquid
---
layout: null
pagination:
  data: collections.post
  size: 1
  alias: item
permalink: "{{ item.date | NewBase60: item.data.categoryCode, collections[item.data.category] }}/"
---

<!DOCTYPE html><html><head><meta http-equiv=refresh content="0; url={{ item.url }}">
Nunjucks
---
layout: null
pagination:
  data: collections.post
  size: 1
  alias: item
permalink: "{{ item.date | NewBase60(item.data.categoryCode, collections[item.data.category]) }}/"
---

<!DOCTYPE html><html><head><meta http-equiv=refresh content="0; url={{ item.url }}">

Eleventy Filter

Almost there now.

We need to let Eleventy know that this file exists so it can be used as a Filter in Layouts, Content, etc., so we’ll save the complete code to a JavaScript file somewhere in our project:

See the complete Eleventy Filter
// 60 characters that make up the Sexagesimal numeral system
const SEQUENCE = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz"

// Converts a Decimal (Base 10) Integer to a Sexagesimal (Base 60) String
const DecimalToSexagesimal = (value) => {
	if (value === undefined || value === 0) {
		return 0
	}
	let sexagesimalValue = ""
	while (value > 0) {
		let index = value % 60
		sexagesimalValue = SEQUENCE[index] + sexagesimalValue
		value = (value - index) / 60
	}
	return sexagesimalValue
}

// Converts a JS Date Object to a Sexagesimal (Base 60) String
const DateToSexagesimal = (dateObject) => {
	let sinceEpoch = dateObject.getTime()
	let epochDays = Math.floor(sinceEpoch / (1000 * 60 * 60 * 24))
	return DecimalToSexagesimal(epochDays)
}

// Export a function for an Eleventy Filter
module.exports = (date, categoryPrefix, collection) => {
	// Get all posts where DATE matches in UTC
	const postsToday = collection.filter((post) => {
		if ("date" in post.data) {
			return new Date(post.data.date).toLocaleDateString("en", { timeZone: "UTC" }) === new Date(date).toLocaleDateString("en", { timeZone: "UTC" })
		}
		return false
	})

	// Get the index of the post where EPOCH TIMESTAMP matches
	// Note: Indices start at 1 for ShortURLs
	const postIndex = 1 + postsToday.findIndex((post) => {
		return new Date(post.data.date).getTime() === new Date(date).getTime()
	})

	// Build the string
	return categoryCode + DateToSexagesimal(date) + postIndex
}

Next, we’ll pull it into our Eleventy Config and add it as a Filter:

const NewBase60 = require("./YOUR_CHOSEN_PATH/NewBase60.js")

module.exports = (eleventyConfig) => {
	eleventyConfig.addFilter("NewBase60", NewBase60)
}

That’s all, folks!

You can now use it in your posts’ Layout or wherever else you have access to a post’s metadata, e.g. looping through a collection. Here’s a Liquid and Nunjucks example demonstrating how to create a link to a post’s ShortURL that the user can copy and share:

Liquid
{% assign shorturl_id = page.date | NewBase60: categoryCode, collections[category] %}
<a href="http://repc.co/{{ shorturl_id }}">
    repc.co/{{ shorturl_id }}
</a>
Nunjucks
{% set shorturl_id = page.date | NewBase60(categoryCode, collections[category]) %}
<a href="http://repc.co/{{ shorturl_id }}">
    repc.co/{{ shorturl_id }}
</a>

In Closing

I think the real heavy-lifting in this solution is in its use of the Sexagesimal numeral system and its ability to express a long Decimal number with far fewer Sexagesimal digits. In our case, because we’re representing the number of days since Epoch, that means that our Sexagesimal number will be only 3-digits long (making the ShortURL only 5-characters long) until…

That’s some longevity I can live with! … and, presumably, die with!

What comes next?

The next major step is to complement what we’ve built here with a website to handle redirecting ShortURLs to their respective blog posts.

In an upcoming Part Two, we’ll dive deeper into how I built that for myself and how we can maintain a feeling of control over what we build by removing barriers to publishing through catering our code to our precise, personal needs.