Tailwind-to-head with Eleventy

This follow-up to an earlier article about Hugo Pipes shows how to get internal CSS in an Eleventy site — and with no build tools required.


Last month, I posted “Tailwind-to-head with Hugo Pipes,” an explanation of how to achieve internal CSS — styles injected into the HTML head rather than existing in a separate .css file — in a Hugo-based site. In particular, this procedure involved CSS from the popular Tailwind CSS framework, although I also showed how to do it with SCSS. Each method was easy because of Hugo’s built-in asset pipeline, Hugo Pipes.

By contrast, Eleventy, the other popular static site generator I typically recommend, has no such asset pipeline. As a result, more tech-savvy Eleventy users (and, given Eleventy’s popularity among the web dev crowd, they are legion) often try to achieve similar results through use of build tools like webpack, Parcel, and Gulp, among others.

The thing is, using those suckers can be a hassle, regardless of one’s abilities. Besides, I try to push simpler solutions whenever possible, as I did last year when I converted this site from an Eleventy/webpack setup to one that was Eleventy-only.

So I got to thinking: what if we just achieved it through an Eleventy project’s package.json file? What if we used package.json scripting to serve as an asset pipeline of sorts for Eleventy?

After all, we’ve already seen how to do that with SCSS, using the sass distribution that enables SCSS use in Eleventy. How much more difficult, I wondered, could it be to do the same via the PostCSS tool required for use of Tailwind?1

Fortunately, the answer turned out to be: “not so difficult” — at least, that was the case once I stopped doing dumb things. I’ll spare you that sob story and cut to the chase.

The package.json part

First of all, let’s cover the package.json scripting (I’ll save space by not including the testbuild scripts I also use for my own nerdy purposes):

	"scripts": {
		"clean": "rimraf _site src/_includes/css",
		"start": "NODE_ENV=development npm-run-all clean --parallel dev:*",
		"build": "NODE_ENV=production npm-run-all clean postcss-build --parallel prod:*",
		"postcss-build": "postcss src/assets/css/index.css -o src/_includes/css/index.css --config ./postcss.config.js",
		"dev:postcss": "postcss src/assets/css/index.css -o _site/css/index.css --config ./postcss.config.js -w",
		"dev:eleventy": "ELEVENTY_ENV=development npx @11ty/eleventy --watch --quiet --serve",
		"prod:postcss": "postcss src/assets/css/index.css -o src/_includes/css/index.css --config ./postcss.config.js",
		"prod:eleventy": "ELEVENTY_ENV=production npx @11ty/eleventy --output=./_site"

Update, 2021-03-24: I corrected the script dev:eleventy, above, so that it includes the --serve parameter rather than the --watch parameter; as I was reminded on Twitter and is explained in the Eleventy documentation, serve includes the “watching” process, so it’s unnecessary to have both --watch and serve. Sorry that I missed this earlier, which probably happened because I previously was using a separate BrowserSync instance and, thus, the code from which I was copying at the time didn’t have (or need) the --serve parameter for Eleventy.

Now let’s see what all those scripts do when you invoke either development mode (npm run start) or production mode (npm run build). Here’s the resulting sequence of actions; they’re more alike than not alike, so I’ll combine them into one list:

  1. Tell the process whether it’s in development or production mode.
  2. Delete stuff left behind from previous deployments. (The rimraf package makes this more friendly across different OSs than the more macOS- and *n*x-specific rm -rf.)
  3. In production mode only, run PostCSS to write a CSS file in the site-wide includes directory. This is where we’ll get the CSS that we’re going to inject in the head.2
  1. From here, we do all of the following simultaneously (using the --parallel option in the npm-run-all tool):

    a. Run PostCSS to write a CSS file in the place where the given environment expects to see it (that’s right: if you’re in production mode, we’re doing this again to be safe). I already told you the location for the production environment; for the dev environment, it’ll be /css/index.css in the final overall site location (for which _site is the Eleventy default). In development mode only, PostCSS also will watch for your edits to the source files and rewrite the CSS file accordingly.

    b. Run Eleventy to write your site’s HTML files. In development mode only, Eleventy will also run a local BrowserSync-based web server which will watch for your edits to the source files (templates, Markdown, etc.) and rewrite/serve the HTML files accordingly.

That’s it for package.json. How do we then go get that CSS for the actual HTML pages themselves? Let’s head to the site-wide head.3

How head handles it

In the head, we tell Eleventy where to look for the CSS and how to use it. In essence, we want to instruct the head as follows:

  1. Tell it whether we’re in development or production mode.
  2. If we’re in development mode, link to the site’s /css/index.css file. (Since we are in dev mode, we’re not worried about that extra render-blocking resource.)
  3. If we’re in production mode, inject the contents of the project’s src/_includes/index.css file between <style> and </style>.

In my Eleventy repos, I use both Nunjucks and pure-JavaScript (.11ty.js) templating, so I’ll show you how to include these instructions in each. Those of you who use other templating approaches from among Eleventy’s numerous offerings can adapt for your use the following examples (most likely the Nunjucks examples, especially if you’re using the Liquid templating language).

In Nunjucks templating

So Nunjucks can detect the environment, you first must create a file in the Eleventy project’s global data directory (usually /_data) to expose this information to Nunjucks. In this example, we’ll create the file /_data/projEnv.js with the following content:

module.exports = {
	environment: process.env.ELEVENTY_ENV

(You may want to read in the Eleventy documentation about how to expose environment variables.)

Now, at the top of the head.njk template file, add:

{% set eleventyEnv = projEnv.environment %}

Then, down in the section where you’d want to call your CSS, add:

{% if eleventyEnv == "production" %}
	<style>{% include 'css/index.css' ignore missing %}</style>
{% else %}
	<link rel="stylesheet" href="/css/index.css" type="text/css" />
{% endif %}

The include statement starts in the site-wide includes directory, so it’s pulling from where PostCSS created the production-mode CSS file.

In JavaScript-only templating

Unlike what we did in the Nunjucks templating, there’s no need for specially exposing the environment setting in JavaScript-only templating: the JavaScript we’ll add will “read” it from the package.json scripting.4 Thus, we can proceed directly to head.js5, in which you add the following at the top:

const fs = require('fs')
var internalCSS = ''
var internalCSSPath = 'src/_includes/css/index.css'
if (process.env.NODE_ENV === 'production') {
	if(fs.existsSync(internalCSSPath)) {
		internalCSS = fs.readFileSync(internalCSSPath)

Then, down in the section where you’d want to call your CSS, add:

${ process.env.NODE_ENV === 'production'
	? `<style>${internalCSS}</style>`
	: `<link rel="stylesheet" href="/css/index.css" type="text/css" />`

“Hey, not so fast”

Those of you who are the least bit OCD-ish about continuity may be wondering, “Um, didn’t you say a few weeks ago that you’d changed this site over to Hugo and SCSS so you could get away from not only Tailwind but pretty much everything with software dependencies?”

Yes. Yes, I did. So what happened? Well, simply, things changed for me after that — quite a bit, in fact.

Here’s the TL;DR version:

  • As I’ve noted previously, it now appears the Day Job will entail my working a lot more with, you got it, dependencies-heavy development — in JavaScript and HTML, specifically.
    (Update, 2021-04-15: After an initial miscommunication gave me the wrong impression about the thinking higher up, I learned that I won’t be doing this work, after all. However, I’m leaving this segment in place for archival purposes and for the sake of transparency.)
  • For my part, this is actually a good thing, because it means we may finally get to do what I’ve been advising since 2019, which would shed a ton of technical debt while greatly improving our websites’ performance. Discussions and resulting decisions remain in the future, but things are looking promising.
  • I also am intrigued by the announcement — and the performance I’ve personally witnessed — of the new, experimental just-in-time compiler for Tailwind. It makes Tailwind in a dev environment much faster, almost as fast as working in SCSS but with the advantages of utility-first CSS.

Accordingly, I’ve decided to dip my toes back into the piranha tank of dependencies-heavy development, with the hope that I continue to be ambulatory thereafter.

All of the above being the case, I also decided that I’d benefit more (consider it cross-training, if you will) from doing my personal stuff in a JavaScript-based SSG once again rather than Hugo with its basis in the Go language.

And, while I briefly considered trying to convert my site over to Next.js since that’s the framework we may very well adopt at the Day Job for the corporation’s sites, I found it far more trouble than it was worth for a simple Markdown-based site like this one.6

. . . which, of course, has “danced” me back to Eleventy and Tailwind, albeit Tailwind with the JIT compiler. So there y’go.

  1. I even borrowed the featured image (for social posts, even when the site’s current configuration isn’t showing featured images) from the earlier “Tailwind-to-head in Hugo Pipes” post since its subject and this post’s subject are essentially so similar. You can call it being lazy; I prefer to say I was “leveraging the image’s thematic synergy.” (Hey, I didn’t spend 30 years in tech marketing for nothing, you know.) ↩︎

  2. This step makes sure there is a CSS file there, since the previous step killed anything from your earlier work. While you can code around a no-file-there situation, you could end up with ugly, CSS-less pages. Why bother? This solves the problem. ↩︎

  3. At least, I’m assuming you have the same head for your site through your templating process. If you don’t, this’ll be a bit hairier for you; but, if you’ve gone to the trouble to assign different heads to different pages for some reason, I doubt you really need my help with all this in the first place. ↩︎

  4. It’s worth noting that Nunjucks will detect the Eleventy environment while this JavaScript will detect the Node.js environment. However, we specify both environments in the package.json scripts, so we get the desired results either way. ↩︎

  5. This file structure is based on the work of Reuben Lillie, as I’ve described before↩︎

  6. Let’s just say that you don’t want to start from scratch, knowledge-wise, and try to make a Next.js site understand a path like /posts/2021/03/this-is-my-post-title. It’s actually pretty easy if you’re dealing with HTML-in-JS files, thanks to the really clever routing built into Next, but not so much with Markdown files. It’s not lost on me that nearly every blogging example for Next.js you can find out there has its Markdown files in only one level. But nested levels? Ha. ↩︎