Inspecting an existing 11ty project
04 Oct 2021
Cassey Lottman’s “Diving into an existing 11ty project” from the 11ty Meetup is great. You can skim a TL;DW on Cassey’s blog, but I recommend listening to the talk, because there’s a bit more “why” behind each of the 6 concepts to orient yourself to in someone else’s 11ty project.
As a case study, let’s follow Cassey’s methodology to look at web-site-11ty-07-movable-git, the most recent 11ty example code I posted on GitHub (which supports my article “Jekyll doesn’t do components? Liar!“).
1. Installation
My site has a package.json file – use Node Package Manager commands, not Yarn package manager commands, to install the Node.js (JavaScript) libraries (Eleventy, Cross-Env, & Dotenv) that my sample site depends on onto your local computer. Doing so allows you to run the site in development mode as https://localhost
.
Interestingly, at the time I wrote this article, my package.json
used a lot of npx eleventy
, but Cassey pointed out that I needed to change that to npx @11ty/eleventy
. It seems I’ve only been getting away with npx eleventy
because Netlify and my local installation of @11ty/eleventy
know what to do, so I guess I have a lot of retrofitting to do on all my published 11ty repos. Whoops!
2. Inputs & outputs
Outputs
I can’t remember when or why I started doing this, but for some reason, I always hard-code my .eleventy.js
configuration files to write output to a folder called dist
instead of the default folder called _site
(which also happens to be Jekyll’s default output folder name).
// Clarify which folder is for input and which folder is for output
return {
dir: {
input: "src",
output: "dist",
},
};
Gatsby outputs to a folder called public
, so I really couldn’t tell you whose random 11ty starter site I seem to have picked this habit up from back when I was learning 11ty.
Maybe I was trying to use an output folder that made sense but wasn’t the default of any major static site generator. That sounds like something I’d do.
Inputs
I don’t bother, in this project, to change where 11ty finds “include” templates (HTML that goes inside things), “layout” templates (HTML that goes around things), or “data” files, but Cassey explains where to look to verify that I didn’t.
3. Configuration
- I enable parameters for includes
eleventyConfig.setLiquidOptions({ dynamicPartials: true, strict_filters: true, });
- I set up a couple of universal shortcodes that make it easier to leverage little bits of JavaScript like
JSON.stringify(paramHere)
andString(Date.now())
and{ ...paramHere }
.// Make it easy to throw a build-timestamp into my HTML eleventyConfig.addShortcode("version", function () { return String(Date.now()); }); // JSONify filter eleventyConfig.addShortcode("jsonify", function (text) { return JSON.stringify(text); }); // getObjectSpread filter eleventyConfig.addFilter("getObjectSpread", function (obj) { return { ...obj }; });
- I make sure images, hand-written JavaScript, hand-written CSS, etc. have a way of getting into my production site:
eleventyConfig.addPassthroughCopy("./src/static/");
- Since I make heavy use of
*.11tydata.json
files when I use 11ty (more on that under “gotchas” below), I always include this:// Get proper data into .11tydata.json files eleventyConfig.setDataDeepMerge(true);
- I don’t want to let Git tracking ignore my
/README.md
, but I don’t want 11ty to try to mess with it (although I’m pretty sure that might not actually be an issue anymore now that I put things in asrc
folder and setinput
tosrc
), so I tend to use an explicit/.eleventyignore
file and then tell 11ty not to skip over processing files just because they’re ignored by Git:module.exports = function (eleventyConfig) { eleventyConfig.setUseGitIgnore(false); };
I don’t typically do any of the things Cassey demonstrates as examples of configuration, like explicitly declare which template formats I plan to use, putting my layout files in nonstandard places, running my Markdown through a custom Markdown engine, etc.
One exception is an experiment I’ve been working on, trying to migrate a site from Jekyll to 11ty. (I haven’t quite gotten it working yet.)
With migrations, it can be handy to put my includes and layouts in the same folders they started in over in Jekyll, and just change where 11ty expects to see them. But I don’t believe I’ve done that in any public repositories.
4. Debugging
Interesting – it looks like not only is my script
in package.json
for debugging using an older style of debugging, but it’s not even compatible with the Windows environment on which I typically develop.
I should probably change this:
{
...
"scripts": {
...
"debug": "DEBUG=* npx eleventy"
}
...
}
To this:
{
...
"scripts": {
...
"debug": "cross-env DEBUG=* npx @11ty/eleventy"
}
...
}
Honestly, it doesn’t matter much, because not only have I never used the new {{ myTemplateVariableName | log }}
syntax … I haven’t even used debug
mode.
5. Style processing
Plain old CSS
I don’t do any special CSS processing in this one.
There’s just this in .eleventy.js
:
// Pass things straight through from "src" to "dist"
eleventyConfig.addPassthroughCopy("./src/static/");
And a /src/static/css/global.css
file:
.pink-div {
background-color: #d81b60;
color: black;
}
.blue-div {
background-color: #1e88e5;
color: white;
}
.task.task-odd {
background-color: #ffc107;
color: black;
}
.task.task-even {
background-color: #004d40;
color: white;
}
And a <LINK>
tag consuming it in the HTML at /src/_includes/layouts/base.liquid
:
<!DOCTYPE html>
<html>
<head><link rel="stylesheet" href="/static/css/global.css"/></head>
...
</html>
Tailwind CSS
But I definitely have plenty of preprocessing going on in the “11ty-tailwind-jit” repository. Cassey walks you through figuring out what people like me are up to in 11ty codebases like that.
In my “Tailwind” case (which might be out-of-date as of Tailwind CSS version 2.2… I should look into whether somehow using the Tailwind CLI is useful…):
- The
.eleventy.js
config file, I tell 11ty that when it’s running locally in “development” mode, it needs to rebuild when it sees that PostCSS has altered the contents of/dist/tailwindoutlive.css
.// Because we're running PostCSS as a separate process, Eleventy doesn't know when styles have changed. // Tell Eleventy to watch this CSS file so it can live-update changes into the browser for us. eleventyConfig.addWatchTarget("./dist/tailwindoutlive.css");
- The
scripts
in thepackage.json
file all ensure that an environment variable namedNODE_ENV
gets explicitly set toproduction
ordevelopment
before running 11ty, which is something Tailwind CSS relies on to decide how it should run. - There’s a
/utils/postcss.js
file that actually makes running Tailwind CSS with PostCSS happen. - There’s a
/_data/tailwindcss.js
file importing the/utils/postcss.js
util the same way it might import, say, the “Sanity client” library or a “Contentful client” library. It passes this utility function the contents of the/src/tailwind/tailwind.css
file (a pretty standard type of file to have in a Tailwind project).let thePostCSS = require("../../utils/postcss.js"); module.exports = async function () { return await thePostCSS("tailwind/tailwind.css", (input) => input); };
-
There’s a “computed pseudo-page” (see “gotchas” below) whose job is to summon this
tailwindcss.js
file and turn its output into a URL called...mysite.com.../tailwindoutlive.css
. I’ve put it at/src/computed_pseudo_pages/singleton_css_file.njk
.--- permalink: /tailwindoutlive.css --- {{ tailwindcss | safe }}
- There’s a
/src/_includes/layouts/html_head.liquid
file that makes sure...mysite.com.../tailwindoutlive.css
gets used by every page on the site:<link rel="stylesheet" href="/tailwindoutlive.css"/>
- There’s a
tailwind.css
file, which is a pretty common file to have in a Tailwind project, at/src/tailwind/tailwind.css
specifically. In this project specifically, a “data” file used in a singleton-page template explicitly passes the contents of this CSS file to the “util” that actually knows how to run Tailwind CSS.@tailwind base; @tailwind components; @tailwind utilities; a { @apply text-indigo-500; }
- There’s a
tailwind.config.js
file, which is a pretty common file to have in a Tailwind project, at/src/tailwind/tailwind.config.js
specifically. The most important thing in this one is that it tells Tailwind to look for Tailwind-esque vocabulary likebg-red-500
inside of all files found in certain filepaths that end in certain file extensions (HTML underdist
or Liquid/Nunjucks/HTML undersrc
). The “util” file I mentioned above – the one that “knows how to run Tailwind CSS – is hard-coded with the path to this file in it, as is/src/postcss.config.js
, which I’ll mention next.module.exports = { purge: { content: [ "./dist/**/*.html", "./src/**/*.liquid", "./src/**/*.njk", "./src/**/*.html", ], options: { safelist: [], }, }, };
- Finally, there’s a
/src/postcss.config.js
file, which is a pretty common file to have in a Tailwind project. Honestly, at this point, even reading my own code, I don’t remember what explicitly calls it (or if there’s something implicit that will be looking for it there). But things work if I put it there. Man – 9 files, most of which I barely understand the interconnections of, 1 of which I can’t even find the interconnection to … I really ought to see if the new Tailwind v2.2 CLI can let me refactor to a simpler architecture.module.exports = { plugins: [ require(`@tailwindcss/jit`)(`./src/tailwind/tailwind.config.js`), require(`autoprefixer`), ], };
6. Gotchas
The data cascade! Cassey covers the data cascade under “gotchas.”
I love doing weird stuff with the data cascade in 11ty, because I love feeling like I don’t have to worry too much about matching the folder names in which I store various files to the URLs that end up in my site.
It makes me feel more prepared for redeveloping the site in another site generator, or for flipping the source of data between filesystem-based data and headless-API-based data.
Markdown folder
I keep all of my .md
files that are intended to be transformed into 1 URL per file in sub-folders of folder called /src/markdown
, like advanced
and basic
. I then use 2 directory data files apiece within those sub-folders to control exactly how the .md
files turn into URLs:
- File:
/src/markdown/advanced/advanced.11tydata.js
ensures there’s no/markdown/advanced/
in the URL for “advanced” pages:module.exports = { eleventyComputed: { documentData: (data) => { return data; }, permalink: (data) => { return `${( data.page.filePathStem.endsWith('/index') ? '' : '/')}/index.html`; }, }, };
- File:
/src/markdown/advanced/advanced.11tydata.json
forces “advanced” pages to use my “advanced template” as a layout, and ensures that they get added to the “pages” collection so my standard footer can easily link to them as a sort of sitemap.{ "layout": "layouts/advanced_page.liquid", "tags": ["pages"] }
- File:
/src/markdown/basic/basic.11tydata.js
ensures there’s no/markdown/basic/
in the URL for “basic” pages:module.exports = { eleventyComputed: { documentData: (data) => { return data; }, permalink: (data) => { return `${( data.page.filePathStem.endsWith('/index') ? '' : '/')}/index.html`; }, }, };
- File:
/src/markdown/basic/basic.11tydata.json
forces “basic” pages to use my “basic template” as a layout, and ensures that they get added to the “pages” collection so my standard footer can easily link to them as a sort of sitemap.{ "layout": "layouts/basic_page.liquid", "tags": ["pages"] }
I do an even fancier variation on this in the “web-site-11ty-05-markdown-pets” repository, where “cats” and “dogs” share some characteristics as “pets”.
Deep data merging
Don’t forget – *.11tydata.*
files usually rely upon deep data merging, so remember that this will probably be in the .eleventy.js
configuration file of any project that gets really fancy with template/directory data files.
// Get proper data into .11tydata.json files
eleventyConfig.setDataDeepMerge(true);
Computed pseudo-pages folder
Then there’s my /src/computed_pseudo_pages/
folder, which you can see several examples of:
- Repo:
web-site-11ty-05-markdown-pets
- Repo:
web-site-11ty-06-podcast
- Repo:
web-site-11ty-07-movable-git
- Repo:
11ty-tailwind-jit
.
- Singletons: All files that directly set a
permalink
value in their front matter use a filename that starts withsingleton_
. - Loops: All files that leverage pagination to set a
permalink
value for iterations over the data in a/_data/
file or an 11ty collection use a filename that starts withloop_
.
Further reading
- If you haven’t watched Stephanie Eckles set up an 11ty site’s architecture from scratch in 3 minutes, you need to. More from her at 11ty Rocks, and it looks like she’s published a slightly slower 20-minute set of videos about setting up an 11ty site from scratch over at Egghead.io.
- Stephanie also published a sample 11ty repository she calls the Smol Starter that’s very similar to the architecture I love (uses both
.md
files and/_data/*.js
API fetches in the same website). - Jérôme Coupé shared the structure he uses on all his 11ty projects
- Once you’re comfortable finding your way around 11ty sites, Bryan Robinson’s blog always has really great ideas on how to leave a nightmare of amazing-but-rare ideas for the next developer to have to reverse-engineer.
- See also Sia Karamalegos’s Architecting data in Eleventy, a nerdy dive into the “data cascade.”
- For more content on how to build complex things that the next developer has to try to figure out, check out Piccalbreak things & leave them cool-but-head-scratching for the next developer, there’s Andy Bell’s Learn Eleventy From Scratch course, but I personally haven’t taken the time to go all the way through it. I sorta found patterns I liked and stopped digging into other ways of doing things in 11ty. And seriously, be sure to invest 3 minutes to watch Stephanie before you invest days in an e-course, even though both have their advantages.