Skip to content

Notes

I’ve started incorporating sample PNGs to illustrate typefaces in the Atlas of Type, and secretly hoped they’d work as OpenGraph previews just as well. Turns out tightly cropped, black-on-transparent images like the one below were not on most platforms’ og:image bingo cards:

A two-line preview of the Betània Patmos typeface, rendered in two lines in black against a transparent background

The sample PNG for the recently-released Betània Patmos, emphasised with a 1px border.

The results were uniformly atrocious. To render well, the samples need a solid background and some padding:

A better og image card shows the typeface against a white background with some padding applied.

As the Atlas is a 100% static Eleventy website, these preview images need to be computed beforehand, ideally without manual involvement. A scan of the docs for the trusty eleventy-img plugin revealed that the 6.x release last week coincidentally added access to sharp, the underlying image processing library, so compositing the sample PNGs onto a white canvas was no big deal.

This is the .eleventy.js config to define an og_image shortcode that takes an image from static/img/typeface/ and generates a corresponding 1200×800 preview image in static/img/card/:

import path from 'node:path';
import ImagePlugin from '@11ty/eleventy-img';

const TARGET_IMG_WIDTH = 1200;
const TARGET_IMG_HEIGHT = 800;

config.addShortcode('og_image', async function(src) {
	const inputPath = path.join('static', src);
	const img = await ImagePlugin(inputPath, {
		formats: ['png'],
		urlPath: '/img/card',
		outputDir: 'static/img/card',
		transform: async sharp => {
			const metadata = await sharp.metadata();
			const pad_width = (TARGET_IMG_WIDTH - metadata.width) / 2;
			const pad_height = (TARGET_IMG_HEIGHT - metadata.height) / 2;
			sharp
				.flatten({ background: 'white' })
				.extend({
					top: Math.floor(pad_height),
					bottom: Math.ceil(pad_height),
					left: Math.floor(pad_width),
					right: Math.ceil(pad_width),
					background: 'white'
				});
		},
		// disable hashing, return original file name.
		filenameFormat: function (id, src) {
			return path.relative('static/img', src);
		}
	});
	return img.png[0].url;
});

Note: I’m probably losing some performance by disabling the built-in hashing, but I wanted predictable URLs for the og:images. If you don’t care about that, remove the custom filenameFormat() function.

Usage in a Nunjucks template, and the resulting markup:


<!-- template -->
<meta 
	property="og:image" 
	content="{{ site.url }}{% og_image '/img/typeface/my-sample.png' %}"
>

<!-- result -->
<meta 
	property="og:image" 
	content="https://type-atlas.xyz/img/card/typeface/my-sample.png"
>

This is well-trodden ground, but this how I do HTTP 301 redirects in Eleventy, mostly to have a reference for the Apache vs. nginx syntax. I’m sticking with the Hugo aliases convention in the front-matter data:

---
title: My post
aliases:
  - /my-previous-url/
  - /my-other-previous-url/
---

Here’s the Eleventy configuration to gather all the aliases in a collection of { from, to } objects. All trailing slashes are removed from the paths, to make their usage clearer in the redirects.

function stripTrailingSlashes(str = '') {
	return str.replace(/^\/|\/$/g, '');
}

config.addCollection('aliases', function (api) {
	return api
		.getAll()
		.map(page => {
			return (page.data.aliases ?? []).map(alias => {
				return {
					from: stripTrailingSlashes(alias),
					to: stripTrailingSlashes(it.page.url)
				};
			};
		})
		.flat();
});

With the collection in place, it becomes a matter of producing a server-specific redirects file. Apache can use a .htaccess file in the website’s root, produced by the Nunjucks template below:

---
permalink: /.htaccess
eleventyExcludeFromCollections: true
---

ErrorDocument 404 /404.html

<IfModule mod_alias.c>
RewriteEngine On
{% for alias in collections.aliases %}
RedirectMatch 301 ^/{{ alias.from }}/?$ /{{ alias.to }}/{% endfor %}
</IfModule>

When it comes to nginx, producing the set of redirects is only half the story. Let’s first generate a 301.redirects file in the website root:

---
permalink: /301.redirects
eleventyExcludeFromCollections: true
---

{% for alias in collections.aliases %}
rewrite ^/{{alias.from}}/?$ /{{alias.to}}/ permanent;{% endfor %}

This file does nothing by default. It needs to be included in the nginx configuration:

server {
	include path/to/301.redirects;
}

There’s a number of ways to go about it, which I won’t document here. But if the technique invovles leaving the physical 301.redirects file in the website’s root directory, you’ll probably want to make it inaccessible to the outside world with something along the lines of:

location = /301.redirects {
	internal;
}

One gotcha is that aliases added to pages having eleventyExcludeFromCollections: true won’t be included in the redirects file, an aspect I noticed when moving some Atom feeds around. Eleventy doesn’t yet have an API to grab excluded pages, so for now I’ve added manual entries to .htaccess.

Reviewing a recent post about my favorite records of 2024, I noticed something off about two lists rendered next to each other. The unordered list has a larger line height than the ordered one, but only in Firefox. Why’s that?

Firefox dev tools show that the unordered list item has a height of 31 pixels, while the ordered one has just 28px pixels

Firefox devtools showed in the Fonts tab that the marker is using a font named -moz-bullet-font, and not inheriting the font from its ancestors like the Rules panel implied.

The Mozilla Bullet font is defined inline in Firefox’s default stylesheet for HTML, and was introduced three years ago to replace the bullet images previously in use. It contains glyphs for the all the simple symbolic counters: disc, circle, and square, as well as disclosure-open and disclosure-closed for all writing modes.

The Mozilla Bullet font has glyphs for disc, square and circle bullets, as well as disclosure triangles

Using a browser-defined font like this is codified in the spec: When used in list-style-type, a UA may instead render these styles using a UA-generated image or a UA-chosen font instead of rendering the specified character in the element’s own font. If using an image, it must look similar to the character, and must be sized to attractively fill a 1em by 1em square.

The reason why there’s no rule in the default stylesheet to apply this font…

::marker { 
	font-family: -moz-bullet-font; 
}

…is because the marker must pick up the parent’s font-family in all other situations, including when changing the marker’s presentation via the content property. Instead, it’s all handled at the code level, and thus invisible to the Rules tab in devtools. (Technically a bug, but one whose resolution might not justify the effort.)

How to fix the spacing? As with other things that may affect the line height, throwing in line-height: 0 seems to do the trick, and you may very well leave it at that.

::marker {
	line-height: 0;
}

But let’s see if we can align Firefox with what other browsers are doing, and inherit the font in all cases.

::marker {
	font: inherit;
}

One immediate problem with this is the default character sequence used in unordered lists, "• " (bullet followed by space), looks dainty and crammed against the text in most fonts. Mozilla Bullet works around that by defining a wider space character. Similarly, Chrome and Safari seem to use internal adjustments to the spacing to make it look a bit better than straight-up rendering the two characters.

As Šime Vidas points out, the possibilities for spacing list markers are quite limited. Moreover, built-in counter styles such as disc can’t be overriden. The kind of solution robust enough for a CSS reset is therefore a bit verbose, and involves defining new counter styles with better spacing (via space characters, no less). This would be just for fixing the disc list markers:

@counter-style disc-fixed {
	system: cyclic;
	/* bullet character… */
	symbols: \2022; 
	/* …followed by three spaces for better spacing */
	suffix: "   ";  
}

ul, menu, dir {
	list-style-type: disc-fixed;
}

::marker {
	font: inherit;
}

Kind of a hard sell, to be honest. But yeah, that’s where the line-height thing comes from.

I’m a sporadic writer. This year, I only managed a couple of articles between my end-of-year music lists doubling as mileposts, and a decade’s worth of writing fits comfortably on a single page.

To shake things up, I’ve been meaning to carve out a separate section for posts that are shorter, smaller in scope, and more frequent. A bit like what Tom MacWright, Simon Willison, or David Bushell are doing.

So, here we are. The inaugural post in the new Notes section.

If you’re reading this in a feed reader and you’ve subscribed before December 2024, you’re receiving the combined feed that includes articles, my linkblog, and these notes. If it suits you better, you can now subscribe to individual feeds.