Rebuilding my blog in JavaScript: feeds
25 May 2023 | 7:43 pm

Two weeks ago I posted about using JavaScript as a templating language in 11ty, as a “fun” learning experiment. I showed off the base layout and the start of the home page template. Since then, I’ve made significant progress:

  • A general page layout that I can use for info pages, like About, Contact, 404, etc.
  • The blog template, listing all posts categorized by year and complete with pagination
  • Layout for blog posts
  • 338 lines of CSS
  • A complicated template setup for feeds

That last item was challenging—more evenings were spent on the feed templating than the rest of the project combined. I had a laundry list of features:

  • Feeds for other sections besides the blog (although right now it’s just the blog)
  • The “Reply via email” link, but I wanted to turn it off per section
  • A configurable limit on the number of entries per feed
  • Valid Atom format based on the spec outlined in RFC 4287

The file structure (yes there’s multiple) ended up looking like this:

  • _includes/layouts/feed.11ty.js—the base layout for the feed
  • content/blog/feed.11ty.js—the template for the feed itself
  • _includes/shortcodes/feedItems.js—a reusable shortcode to create the markup for feed entries

And the way they’re used looks like this:

+-------------------------------+
| layouts/feed.11ty.js          |
| +---------------------------+ |
| | blog/feed.11ty.js         | |
| | +-----------------------+ | |
| | |shortcodes/feedItems.js| | |
| | +-----------------------+ | |
| +---------------------------+ |
+-------------------------------+

I’ll be your tour guide through this spaghetti monster, starting with the shortcode.

The shortcode

Shortcodes in *.11ty.js templates are JavaScript functions that are available globally, which means I can use them pretty much anywhere, and even pass in other shortcodes/functions.

In shortcodes/feedItems.js, I have two top-level functions: escapeHtml() and the main one attached to module.exports.

Here’s what escapeHtml() looks like:

/**
 * Escape HTML entities in a string.
 * @param {String} str - The string to escape.
 * @return {String} - The escaped string.
 * @see {@link https://stackoverflow.com/a/6234804/11985346 Stack Overflow}
 */
function escapeHtml(str) {
	let chars = {
		"&": "&",
		"<": "&lt;",
		">": "&gt;",
		'"': "&quot;",
		"'": "'",
	};
	let keys = Object.keys(chars);
	let regex = new RegExp(`[${keys.join("")}]`, "g");

	return str.replaceAll(regex, (char) => chars[char]);
}

This one starts with an object with a list of characters I need to replace with their respective HTML entities, in order to “escape” them for valid use inside an XML document (the feed).

Then we grab each of the characters with Object.keys() and assign it to the keys variable, which is then passed to the RegExp constructor in the form of a template literal—this way I don’t need to specify the characters twice. Since the output of Object.keys is an array, I used the join() method to strip out the quotes and return a string of characters (&<>"') to build the regular expression I need.

And then in the return statement, I used the replaceAll() method to take the regex and iterate through the character object, replacing each occurrence in str with its respective entity.

Escaping HTML is possible with the Web API in the browser, but things are different in Node—contrary to the name, there is no node interface or DOM manipulation, so it’s duct tape time.

⚠️ Only do this at home ⚠️

Escaping HTML yourself is frowned upon, but since this is a learning project for me, I’m trying to solve every problem myself before adding a dependency—even if the lesson learned is “don’t reinvent the wheel!” yet again. Next time I’ll reach for html-entities or use the filters available in Nunjucks and Liquid.

On to the next function:

/**
 * Returns markup for an Atom feed for a given collection
 * @param {Object} items - Eleventy collection object
 * @param {Object} data - Eleventy data object
 * @param {Number} limit - Number of entries to include
 * @param {Boolean} email - Whether or not to show the Reply via Email link on entries
 * @return {String} - Atom feed markup
 */
module.exports = function (items, email, limit) {
	/**
	 * Limit the items object to the first n entries.
	 * @param {Object} items - Eleventy collection object
	 * @param {Number} limit - Number of entries to include
	 * @return {Object} - Limited Eleventy collection object
	 */
	let entries = items.slice(0, limit);

	/**
	 * Import metadata from _data/metadata.js
	 * @type {Object}
	 */
	const metadata = require("../../_data/metadata.js");

	/**
	 * Generate the entries markup.
	 * @return {String} - Atom feed markup
	 */
	return `
	${entries
		.map(
			(item) => `
	<entry>
		<title>${item.data.title}</title>
		<link href="${metadata.url}${item.url}" rel="alternate" type="text/html"/>
		<id>${metadata.url}${item.url}</id>
		<published>${item.data.page.date.toISOString()}</published>
		<updated>${
			item.data.updated
				? `${item.data.updated.toISOString()}`
				: `${item.data.page.date.toISOString()}`
		}</updated>
		${item.data.description ? `<summary>${item.data.description}</summary>` : ""}
		<content type="html">
			${escapeHtml(item.templateContent)}
			${escapeHtml(
				email
					? `<p><a href="mailto:${metadata.author.email}?subject=Re: ${item.data.title}">Reply via email</a></p>`
					: ""
			)}
		</content>
	</entry>`
		)
		.join("")}
`;
};

I’m pulling in 3 arguments here: items, email, and limit. items is required, the second two will fail gracefully if they’re not passed in when I call the shortcode in my templates later—and by gracefully, I mean there won’t be an email link, and 11ty will happily generate every single item in the collection object.

In the first declaration, I took the items object and sliced it to create a new object with a number of entries between 0 and whatever limit is. Then I assigned that new object to entries. Next, I had to pull in my global data file _data/metadata.js for the site URL and email—not sure why, but global data is either not available to shortcodes, or I can’t find it.

Metadata solved, I iterated over entries with Array.map() in the return statement. The rest is data from the object and one bit of global data for the email address. One important thing I had to do is make sure all the dates were formatted according to the ISO 8601 standard, which the Atom spec requires. That’s accomplished with .toISOString().

The feed template

One thing I like about Hugo is that it creates feeds for sections and tags. I don’t know how to accomplish tag feeds, but for sections, my solution is to create a feed.11ty.js template file in each section I want a feed for, which is currently the blog.

This is the entire contents of the template:

/**
 * @file Defines the blog post feed template
 */

/**
 * Frontmatter in JavaScript templates
 * @type {Object}
 * @see {@link https://www.11ty.dev/docs/data-frontmatter/#javascript-object-front-matter 11ty docs}
 */
exports.data = {
	title: "Blog",
	layout: "layouts/feed.11ty.js",
	eleventyExcludeFromCollections: true,
	permalink: "blog/index.xml",
};

/**
 * Uses the feedItems shortcode to generate an Atom feed for the blog collection.
 * @param {Object} data - Eleventy data object
 * @returns {String} - Atom feed markup
 */
exports.render = function (data) {
	return `
		${this.feedItems(data.collections.blog, true, 1)}
	`;
};

Two things going on here:

  1. I set the title of the feed, told 11ty to use the feed.11ty.js layout for this template, excluded it from collections so it doesn’t get any funny ideas about infinite recursion and render itself inside of itself, and set the permalink to match the existing one on the Hugo version of my site
  2. I called the feedItems shortcode from earlier and passed in the collection I need feed entries for, true for the email link, and the number of entries to return

The feed layout

layouts/feed.11ty.js:

/**
 * @file Defines the template for the RSS feed.
 */

/**
 * Returns markup for an Atom feed for a given collection.
 * @param {Object} data - Eleventy data object
 * @param {Object} collection - Eleventy collection object
 * @returns {String} - Atom feed markup
 */
exports.render = function (data) {
	return `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="${data.metadata.language}">
	<title>${data.title}${data.metadata.title}</title>
	<subtitle>Recent content from the ${data.title} section on ${data.metadata.title}</subtitle>
	<updated>${data.generated}</updated>
	<id>${data.metadata.url}/</id>
	<link href="${data.metadata.url}${data.page.url}" rel="self" type="application/atom+xml"/>
	<link href="${data.metadata.url}/" rel="alternate" type="text/html"/>
	<generator uri="https://www.11ty.dev/" version="${data.eleventy.version}">Eleventy</generator>
	<rights>© ${new Date().getFullYear()} ${data.metadata.author.name}</rights>
	<author>
		<name>${data.metadata.author.name}</name>
		<email>${data.metadata.author.email}</email>
	</author>
	${data.content}
</feed>`;
};

This layout template adds the top level elements in the resulting XML document. The markup doesn’t change between different sections, so it makes sense to make it reusable for the individual feed templates. It gets the title of the feed from the front matter of the feed template, and pulls in values from my global data file to populate the URLs, author name, and email. Then in ${data.content}, it pulls in the contents of the feed template, which would be the blog post entries created by the feedItems shortcode.

Before I end this post, I have to say writing all this out and trying to explain it highlighted issues and things I didn’t need, which is great. I should do this more often.

Reply via email


Disabling macOS mouse acceleration
12 May 2023 | 7:30 pm

Mouse acceleration is a feature that speeds up or slows down the mouse pointer in relation to how fast you physically move the mouse. On Windows, I’ve had this feature turned off for years, because it’s annoying while playing games. On macOS though, you can’t turn it off unless you set tracking speed to 0, which isn’t ideal.

I stumbled upon the open source LinearMouse, which can disable acceleration while keeping tracking speed the same. Now my pointer is motoring around the screen the way I’m used to! No more annoying slowdowns when I’m nearing the end of my range of motion. Feels much more responsive.

I don’t recommend turning it off for the trackpad though, the acceleration is helpful there. On my MX Master 3 it makes a big difference.

Reply via email


Rebuilding my blog in JavaScript
7 May 2023 | 7:07 am

I’m rebuilding my blog with 11ty and writing the templates in JavaScript to get a better grasp of the language. This project is unlikely to see the light of day, but I’m excited that I got a build to succeed with all 212 pages!

One issue I had was assuming 11ty used ES modules. 11ty uses CommonJS modules, which are synchronous, and the syntax is not as intuitive. There’s a solution that depends on an abandoned NPM package, but that wasn’t something I wanted to hang the entire build on. I had to rewrite my imports using Node’s require() method, and exports using module.exports.

I’ve been following Reuben Lillie’s well-documented setup thanks to a tip from Bryce Wray.

While I haven’t gotten into the more complicated stuff, like pagination or image optimization, after hours of fighting errors, I am surprised at how little JavaScript I needed for a base layout. The majority of the file is HTML.

Here’s my base layout, where I’m returning HTML with template literals (${foo}):

module.exports = function render(data) {
	return `<!-- _includes/layouts/base.11ty.js -->
		<!DOCTYPE html>
		<html lang="en">
			<head>
				<meta charset="UTF-8" />
				<meta http-equiv="X-UA-Compatible" content="IE=edge" />
				<meta name="viewport" content="width=device-width, initial-scale=1.0" />
				<meta name="generator" content="${data.eleventy.generator}" />
				<title>${data.title} | ${data.metadata.title}</title>
			</head>
			<body>
				${data.content}
			</body>
		</html>
	`;
};

I’m doing the same with pages, and setting the page title and layout:

class Page {
	data() {
		return {
			title: "Home",
			layout: "layouts/base.11ty.js",
		};
	}
	render(data) {
		return `<!-- content/index.11ty.js -->
			<main>
				<h1>${data.title}</h1>
			</main> `;
	}
}

module.exports = Page;

It’s recommended to use the full file name of the layout, otherwise 11ty will spend extra time cycling through file extensions.

Just wanted to share my progress! I’ll post more updates if the momentum continues.

Reply via email



More News from this Feed See Full Web Site