← Home ← Code

Adding syntax highlighting to this blog

This is an Eleventy blog. I love Eleventy. Not only does it work well, and is everything I could ever ask for, it's also simple and to-the-point. And the cherry on top is the community around it; it's such a lovely bunch.

Anyway, so, the idea of this post is not just showing off the end result of what I ended up with, but instead I will try to document every step I go through. Even the ones that don't work.

Can Eleventy do it?

First step is always to do a search on Eleventy's website. Granted, the search function doesn't always come up with useful results, but for "syntax highlighting" it seems there are plenty results! And yep, it looks like it's been done and it's even an official plugin. To install it, we won't be running npm install … like the Eleventy site tells us to, because this blog is running on Bun (I wanted to try it). Thus, instead of npm install … we'll want to go with the Bun-specific version of that. I don't know what that is, but it's not hard to find in the Bun docs and apparently it's bun add. So, here we go:

bun add @11ty/eleventy-plugin-syntaxhighlight

Next up, we add it to Eleventy through the .eleventy.js config file. I'm running the latest and greatest @11ty/eleventy@3.0.0-beta.1, so we need ESM-style imports instead of the require() the docs suggest.

import SyntaxHighlight from '@11ty/eleventy-plugin-syntaxhighlight'
// + other irrelevant imports

export default function (config) {
	config.addPlugin(SyntaxHighlight)
	// + more stuff, but that was already here
}

Nice! Now let's see if it runs!

Does it work?

Let's serve the blog to see if it worked! Run bun run build (which is defined in my package.json to serve Eleventy), and voilà! Works right of the box. That's Eleventy for you!

Styles

Unfortunately though, there's no styling. The markup works, but we'll need some styles to spice it up and actually make use of the markup. First of all, I feel like we need to clearly distinguish code from regular text. To do so, I first want to give code blocks (and inline code, too) a different background color. Since even my dark mode for the site doesn't have too dark of a background, we can make the background color darker for code. Just playing around with the color picker in the devtools, and I think #182923 will do nicely. I'm trying to avoid creating too many CSS variables, but for this I think I'll make a new one; let's call it --background-color-elevated. I usually try to name things in a way that represents what they do semantically, not just what I use them for; I mean, the background-color part is pretty straight forward, but the -elevated part maybe not so much. If I call it, say, -dark, but then decide to change the colors of my theme and make it lighter than the surrounding background color, then suddenly my CSS variables no longer make sense. Plus, naming them semantically also helps me personally with making sure that I realize when I'm better off creating a new variable, because it becomes about use cases rather than flat values.

Anyway, continuing; for now, we'll set the --background-color-elevated the same for light and dark mode. We'll deal with light mode later. The variables are added to base.css, by the way, which is the stylesheet shared amongst all pages. To then apply it to the code and pre HTML elements, we'll write styles in post.css, which is a stylesheet loaded only on the post pages.

article {
	/* …other already-existing styles… */
	code {
		padding: .2rem .4rem;
		background-color: var(--background-color-elevated);
		border-radius: .3rem;
	}
	pre {
		background-color: var(--background-color-elevated);
	}
}

I've given the code elements some padding and a border radius, but then I realized that the code blocks, while having a root <pre> element, they also have a second <code> container as a direct child of the pre. I don't want the styles for the inline code and block code to affect one another, so I'll instead change the selector of the former to :not(pre) > code. Then, we add some padding and a border radius for the code blocks too (though larger ones), and a margin to both create more space around the surrounding paragraphs, as well as to "indent" it on both sides compared to the text. We end up with

article {
	/* …other already-existing styles… */
	:not(pre) > code {
		padding: .2rem .4rem;
		background-color: var(--background-color-elevated);
		border-radius: .3rem;
	}
	pre {
		padding: 1rem 2rem;
		margin: 1.5rem 1rem;
		background-color: var(--background-color-elevated);
		border-radius: .6rem;
	}
}

Next; tab size. I use tabs in this repository. The markdown files also seem to output tabs. But, the browser renders them with a tab width of 8, which I personally find a bit excessive. So, throw a tab-size: 4; in there!

Before moving on to coloring the actual highlighting, a short interlude.

The CSS highlighting is no good

I recognize the markup that the plugin generates. No doubt, it's Prism. Prism's default highlighting for CSS is, well, lacking, at least in my personal opinion. But, it has a plugin for it, something along the lines of "CSS extended". To add it, first I'll go back to the Eleventy docs for the plugin to see if there are any examples adding Prism plugins (and to see if it is actually Prism).

Well, I was right in that it was Prism. But unfortunately, no simple examples to show how to add plugins. It does however have an init option that takes a callback, allowing us to customize the Prism "global". I've never actually loaded custom languages with Prism, though, I've always just bundled them and downloaded them straight from the Prism website. Time to read the Prism docs, I guess! The CSS package I need is apparently called css-extras (not "extended", whoops), and actually now I'm thinking I might be able to just… tag a code block with css-extras instead and hopfully it automatically imports the language definitions.

Well, nope! That didn't work. Turns out, I just needed to scroll up a bit in the Prism docs (or well, it's just the homepage) to find that I need to import loadLanguages from prismjs/components. I gotta be honest, I don't really like it. Before I go this route, let me check if the plugin exports or exposes something by inspecting the source code.

That's an issue

Turns out, the plugin loads languages automatically. The problem is that the css-extras language is not called css-extras, it just adds a bunch of stuff to the css language. That means I cannot use it by tagging code blocks with css-extras, but if I tag them css, then the extras don't load. To verify that it works this way, I can add css-extras to one block, and css to another, and low and behold! The css block now gets the extras.

Theoretically, I can force it by including a hidden css-extras code block. Alternatively, the "proper" way would be adding prismjs to my dependencies and manually loading css-extras in the init callback:

import loadLanguage from 'prism/components'
import SyntaxHighlight from '@11ty/eleventy-plugin-syntaxhighlight'
// …

export default function(config){
	config.addPlugin(SyntaxHighlight, {
		init: function({Prism}){
			loadLanguage('css-extras')
		}
	})
	// …
}

But honestly I feel like the css-extras would be a good default, and even if the plugin's maintainer doesn't want that, having to install prism manually for this (seemingly basic) use case is pretty awkward in my opinion.

Okay, pull request opened. In the mean time, let's bear with the not-as-good CSS highlighting.

Coloring the code

I know there are pre-built themes to highlight the code. But I want to keep things simpler (and make a lot of the things green). So, we'll have to write our own.

I guess we'll need a few more colors. We can use --text-color-dim, for keywords and whatnot. I also want to highlight strings, and comments. Bit of playing around with colors, and eventually landed on

	--string-color: #b3c17d;
	--comment-color: #938b84;

…which we apply to certain elements inside the pre element like so:

	.tag, .property, .keyword { color: var(--text-color-dim); }
	.string, .attr-value { color: var(--string-color); }
	.punctuation, .comment { color: var(--comment-color); }

For now, that supports the simple CSS syntax and HTML and JS. On second thought, it doesn't even seem like we need to load css-extras. Oh well!

Before we end this post, we need to deal with light mode. So, put on your sunglasses, here we go! Finding nice colors for this was actually kind of difficult, because it turns out it's rather hard to create high-enough contrast between the green elevated color and the text. I could make the background color lighter than the surrounding background, but it didn't look very nice to me.

	--background-color-elevated: #c8f7da;
	/* … */
	--string-color: #736d08;
	--comment-color: #8a6152;

And while we're at it, we can also make comments italic. Gives them a bit more of a comment-ey feel to them, you know what I mean?

Okay, done!

So, I guess we're done here. I could look into adding line numbers or copy-to-clipboard buttons, but to be honest, I don't really care. If you really want to know what line something is one, I trust the reader possesses the ability to count. And the ability to select and copy without a button. Alternatively, feel free to type your own code!

PS: It's now one day later and I've already tweaked things (mostly for better responsiveness). Oh well!