Reading List
The most recent articles from a list of feeds I subscribe to.
E-ink newspaper artwork
What a lovely project! Project E-Ink builds a large e-ink display that displays the front page of the newspaper.
I have a strong attraction towards e-ink. I think it’s because despite being state of the art technology, e-ink doesn’t feel “digital”. In the case of this display: it’s there, it’s connected, but it’s static. It’s up to date but doesn’t grab your attention.
Jeremy Keith: 'With AI, tech has broken the web’s social contract'
Jeremy Keith has an interesting take on how AI affects how we interact with search engines as content creators.
Previously, Google had a mutually beneficial agreement with websites: websites provided content, and Google brought traffic. Now, Google is using our content to generate and host their own.
E-ink newspaper artwork
What a lovely project! Project E-Ink builds a large e-ink display that displays the front page of the newspaper.

I have a strong attraction towards e-ink. I think it's because despite being state of the art technology, e-ink doesn't feel "digital". In the case of this display: it's there, it's connected, but it's static. It's up to date but doesn't grab your attention.
Mozilla wants its documentation to gaslight you

Mozilla is one of the most important companies on the Internet. For a very long time, they have represented the only real competitor that Microsoft and Google have had as far as web browsers go. Mozilla is widely seen as a force for good by vast numbers of the developer community, but they seem to be torching all that good will by just giving up.
One of their most critical resources is the Mozilla Developer Network (MDN). This documents every HTML, CSS, and JavaScript feature so that developers of all skill levels can understand and implement things. To say this is widely used is an understatement. I'd be willing to bet that it's used by developers at Microsoft, Facebook, Google, Amazon, Apple, and all Fortune 500 companies. I personally use MDN more than basically any other resource for learning how HTML, CSS, and JavaScript features work. It is critical load-bearing infrastructure on the Internet.
Recently they added an AI Help feature, and I'm uncertain about it being a good idea. One of the greatest assets that Mozilla has with the MDN team is their technical writers that make fantastic documentation, examples, and breakdowns of every single feature in browsers. This takes an unimaginable amount of work and has lead to creating one of the best resources possible. So obviously, we need to go and take all of that and replace it with the new technical writer that never sleeps, eats, has kids, goes on vacation, or gets burnout: ChatGPT. This opens up the possibility for taking this source of joy and creativity into a source of a new philosophical horror: gaslighting as a service.
As a part of noting my biases, I found out about this issue after being mentioned on Mastodon pointing to a GitHub thread. I was taken aback at first but I honestly do see how this could let skilled human writers speed up in the way that AI lets you avoid the "blank canvas problem".
I've been meaning to write more about this, but the basic idea is that for many people there is nothing more terrifying than a blank canvas, empty editor frame, or camera sitting on a desk. One of the first things you learn as an artist is to use the blank canvas as an asset and get over the fear of it by starting with something, anything to help you get started. Even a single line or circle to position things around.
I think that tools like ChatGPT could legitimately help people get past that "blank canvas" problem by giving them something, anything to start from. When you combine that tool with the genuine skill that the MDN docs team has, this could lead to amazing things.
I don't see that as possible with Mozilla's current leadership and business model. I don't know if Mozilla's business model ever made sense, it makes a lot more sense if it's something closer to a nonprofit rather than a commercial entity. Certianly not something that spends five million dollars per year on the CEO's paycheck. We're just watching Mozilla circle down the drain towards irrelevance.
In 2017 Mozilla released Firefox Quantum, a ground-up reimagining of what using Firefox really means. XUL was excised out of the browser, Rust was introduced to have truly memory safe code in the browser, browser speed was drastically increased, and overall it's one of the best updates that Firefox has ever had or likely will have. I checked the LinkedIn profiles of the major players that made Quantum possible and found out that nearly none of them still work at Mozilla.
There were claims that they were victims of layoffs in 2020 because apparently people weren't using web browsers in the age of COVID. During the age of COVID. When everyone was locked down. At home. Bored out of their minds. With nothing to really entertain them but. The Internet. Which you access with browsers.
Other big-ticket projects from Mozilla are no longer being made in-house. Rust is the biggest one. Rust was made as a language to replace C++ in Firefox and let the browser have actual memory safe code so that we don't run into the perinneal C++ landmines that lead to remote code execution triggering panic updates. Rust is still used in Firefox (and soon Chrome too), but it's no longer a Mozilla project.
Servo was the browser engine to surpass Gecko. It was a ground-up reimagining of how browser engines could work, taking advantage of native GPU hardware features as much as possible. This would allow the browser to render at literal thousands of frames per second and absolutely sip battery power in the process. This would have been an even more profound reimaging of what a browser engine could be that would make even the Quantum update look like a mere recolor in comparison.
But no, the entire Servo team was sacked and Firefox limps on with Gecko. The only real competitor being WebKit and Blink, which are both just forks of forks of the KDE library KHTML.
It really hurts to see Mozilla fall and watch as other browser engines also fall the way of the dodo. I hate living in a world where the only real option for web browsers is Google Chrome. I realize that browser engines are mind-numbingly complicated because they have to be, but good lord it hurts to see diversity in implementation just fade to nil because investors want an infinite money generator.
This is the legacy that our generation will leave. One where true genius and innovation are swept under the rug because they couldn't make a line go up fast enough to make line-obsessed fools only focus on the line.
I'm sad to see such a critical web resource fall prey to hype cycles that rot out the best part of MDN: the technical writers. The technical writing at MDN is seriously some of the best you can get in the industry. I'd pay for a printed version of MDN. But now with this automated gaslighting as a service, I don't think I can justify paying for "MDN Plus" to find new and interesting ways for ChatGPT to lie to me about what CSS features can do. If I wanted to do that, I could just ask Mimi.
Array.prototype.with
to replace a value in an array that I pass to a function?Array.prototype.with is not a valid JavaScript feature. The
Array.prototype object does not include a method called with for
replacing values in an array.If you would like to replace values in an array, you can use the standard JavaScript array methods such as
splice, map, or forEach. These methods can help you
modify specific elements within an array or create a new array with
replaced values.If you provide more details on what you are trying to achieve, such as the specific value you want to replace and the conditions for replacement, I would be happy to provide you with a code example using built-in array methods.
Imagine what developers feel when they get told something exists, then upon trying to get more information about it get told that it does not in fact exist. On the same page.
If this isn't gaslighting as a service, what is? How will the next generation of developers get trained if they are constantly mislead by the shadows of meaning we used to displace human authorship because it's expensive to make sure people can afford to eat?
11ty: Index ALL the things!
This is a second spinoff post in the migration saga of this blog from WordPress to 11ty.
On good URLs
It was important to me to have good, RESTful, usable, hackable URLs. While a lot of that is easy and comes for free, following this principle with Eleventy proved quite hard:
URLs that are “hackable” to allow users to move to higher levels of the information architecture by hacking off the end of the URL
What does this mean in practice?
It means it’s not enough if tags/foo/ shows all posts tagged “foo”, tags/ should also show all tags.
Similarly, it’s not enough if /blog/2023/04/private-fields-considered-harmful/ links to the corresponding blog post,
but also:
/blog/2023/04/should show all posts from April 2023/blog/2023/should show all posts from 2023/blog/should show all posts
Eleventy “Pagination” Primer
Eleventy has a pagination feature, which actually does a lot more than pagination: it’s used every time you want to generate several pages from a single template by chunking object keys and using them in permalinks.
One of the most common non-pagination use cases for it is tag pages. The typical /tags/tagname/ page is generated by a deceptively simple template:
---
pagination:
data: collections
size: 1
alias: tag
filter: ["blog", "all"]
permalink: /blog/tags/{{ tag }}/
override:tags: []
eleventyComputed:
title: "{{ collections[ tag ].length | pluralize('post') }} on {{ tag | format_tag }}"
---
{% set taglist = collections[ tag ] | reverse %}
{# ... Loop over taglist here ... #}
That was it, then you just loop over taglist (or collections[ tag ] | reverse directly) to template the posts under each tag in reverse chronological order.
Simple, right?
But what about the indices?
As it currently stands, visiting /blog/tags/ will just produce a 404.
Index of all tags
Creating an index of all tags only involves a single page, so it does not involve contorting the pagination feature to mind-bending levels, like the rest of this post. However, we need to do some processing to sort the tags by post count, and remove those that are not “real” tags.
There are many ways to go about with this.
The quick and dirty way
The quick and dirty way is to just iterate over collections and count the posts for each tag:
<ol>
{% for tag, posts in collections %}
<li>{{ tags.one(tag) }}
({{ posts.length }} posts)
</li>
{% endfor %}
</ol>
Unfamiliar with the tags.one() syntax above?
It’s using Nunjucks macros (there’s a {% import "_tags.njk" as tags %} earlier in the template too).
Macros allow you to create parameterized templates snippets,
and I’ve come to love them during this migration project.
The problem is that this does not produce the tags in any particular order, and you usually want frequently used tags to come first. You could actually fix that with CSS:
<ol>
{% for tag, posts in collections %}
<li style="order: {{ collections.all.length - posts.length }}">
{{ tags.one(tag) }}
({{ posts.length }} posts)
</li>
{% endfor %}
</ol>
The only advantage of this approach is that this is entirely doable via templates and doesn’t require any JS,
but there are several drawbacks.
First, it limits what styling you can use: for the order property to actually have an effect, you need to be using either Flexbox or Grid layout.
But worse, the order property does not affect the order screen readers read your content one iota.
Dynamic postsByTag collection
To do it all in Eleventy, the most common way is a dynamic collection,
added via eleventyConfig.addCollection():
config.addCollection("postsByTag", (collectionApi) => {
const posts = collectionApi.getFilteredByTag("blog");
let ret = {};
for (let post of posts) {
for (let tag of post.data.tags) {
ret[tag] ??= [];
ret[tag].push(post);
}
}
// Now sort, and reconstruct the object
ret = Object.fromEntries(Object.entries(ret).sort((a, b) => b[1].length - a[1].length));
return ret;
});
That we then use in the template:
<ol>
{% for tag, posts in collections.postsByTag %}
<li>
{{ tags.one(tag) }} ({{ posts }} posts)
</li>
{% endfor %}
</ol>
Custom taglist filter
Another way is a custom filter:
config.addFilter("taglist" (collections) => {
let tags = Object.keys(collections).filter(filters.is_real_tag);
tags.sort((a, b) => collections[b].length - collections[a].length);
return Object.fromEntries(tags.map(tag => [tag, collections[tag].length]));
});
used like this:
<ol>
{% for tag, posts in collections | taglist %}
<li>
{{ tags.one(tag) }} ({{ posts }} posts)
</li>
{% endfor %}
</ol>
Usually, filters are meant for more broadly usable utility functions, and are not a good fit here. However, the filter approach can be more elegant if your use case is more complicated and involves many different outputs. For the vast majority of use cases, a dynamic collection is more appropriate.
Index of posts by year
Generating yearly indices can be quite similar as generating tag pages.
The main difference is that for tags the collection already exists (collections[tag]) whereas
for years you have to build it yourself, using addCollection() in your config file.
This seems to come up pretty frequently, both for years and months (the next section):
- https://github.com/11ty/eleventy/issues/502
- https://github.com/tomayac/blogccasion/issues/19
- https://github.com/11ty/eleventy/issues/316#issuecomment-441053919
- https://github.com/11ty/eleventy/issues/1284
- Group posts by year in Eleventy (Blog post)
This is what I did, after spending a pretty long time reading discussions and blog posts:
eleventyConfig.addCollection("postsByYear", (collectionApi) => {
const posts = collectionApi.getFilteredByTag("blog").reverse();
const ret = {};
for (let post of posts) {
let key = post.date.getFullYear();
ret[key] ??= [];
ret[key].push(post);
}
return ret;
});
and then, in blog/year-index.njk:
---
pagination:
data: collections.postsByYear
size: 1
alias: year
permalink: /blog/{{ year }}/
override:tags: []
eleventyComputed:
title: "Posts from {{ year }}"
---
{% import "_posts.njk" as posts %}
{{ posts.list(collections.postsByYear[year], {style: "compact"}) }}
You can see an example of such a page here: Posts from 2010.
Bonus, because this collection is more broadly useful, I was able to utilize it to make a little yearly archives bar chart!

Index of posts by month
Pagination only works on one level: You cannot paginate a paginated collection (though Zach has a workaround for that that I’m still trying to wrap my head around). This also means that you cannot easily paginate tag or year index pages. I worked around that by simply showing a more compact post list if there are more than 10 posts.
However, it also means you cannot process the postsByYear collection and somehow paginate by month.
You need to create another collection, this time with the year + month as the key:
config.addCollection("postsByMonth", (collectionApi) => {
const posts = collectionApi.getFilteredByTag("blog").reverse();
const ret = {};
for (let post of posts) {
let key = filters.format_date(post.date, "iso").substring(0, 7); // YYYY-MM
ret[key] ??= [];
ret[key].push(post);
}
return ret;
});
And a separate blog/month-index.njk file:
---
pagination:
data: collections.postsByMonth
size: 1
alias: month
permalink: /blog/{{ month | replace("-", "/") }}/
override:tags: []
eleventyComputed:
title: "Posts from {{ month | format_date({month: 'long', year: 'numeric'}) }}"
---
{% import "_posts.njk" as posts %}
{{ posts.list(collections.postsByMonth[month], {style: "compact"}) }}
You can see an example of such a page here: Posts from December 2010.