Organizing the Eleventy config file

Published:
Last edit:
Reading time:
9 min.

Eleventy comes with basic defaults already set up for you. That means you don’t have to do anything to start working: the output folder defaults to _site, and Eleventy looks for your source files in the root directory.

This is okay for very small projects. However, most of my projects get quite large, and they need customizations and dependencies. I also have personal preferences, and Eleventy is quite open about that - you can arrange your input files and folders as you wish and call them whatever you like. Of course, Eleventy must be made aware of these settings.

This article is all about organizing your Eleventy configuration file.

Let’s begin!

Skip table of contents

Table of contents

Create an .eleventy.js config file

Add a new file in the root directory called .eleventy.js (as of Eleventy 2.0 it may also be called eleventy.config.js.).

Let’s make a small adjustment in the folder structure first.

module.exports = function (eleventyConfig) {
  return {
    dir: {
      input: 'src',
      output: 'dist',
      includes: '_includes',
      layouts: '_layouts'
    }
  };
};

Our output folder is now dist, and all our source files go into src.
Also, due to my personal preference, I pull the layouts out of _includes, where they usually live, and make sure that the directories are now next to each other.

This leaves our root to all the files that absolutely have to be there - like package.json and README.md as well as config files of other modules you use in your project.

Structuring the input folder

Create a folder called src in your root directory.

Though we are not going to touch most of the folders, this is what your usual website-project might look like:

│
├── src
│  │
│  ├── _data
│  ├── _includes
│  ├── _layouts
│  ├── assets
│  ├── pages
│  ├── posts
│  ├── projects
│

pages is for your static pages like index.md, about.md, etc., posts contains your blog articles, and projects is just another collection folder we create to make it worthwhile to get the logic out of eleventy.js.

… Because you can configure all your collections, shortcodes and filters right there.

Outsourcing customizations

I want my projects to grow freely without worrying that my config file is getting too cluttered. So I deal with customizations elsewhere and import only the return value of my functions.

My preference is to create a new folder in the root directory, called config.

Another common approach is adding a folder to src called _11ty. I found this in Nicolas Hoizeys’ starter pack11ty. You can name the folder whatever you want and put it wherever you like. In this case, I will go on pretending you made a folder called config in your root directory.

We don’t need to tell Eleventy about the existence of this folder. We just use it export our return values and import them into .eleventy.js.

I introduce two ways to handle this, using collections as an example.

Method 1: Import file and loop over the collection keys

Create a file named collections.js in your config folder.
Now define all the collections you want to use:

module.exports = {
  posts: function (collectionApi) {
    return collectionApi.getFilteredByGlob('src/posts/**/*.md');
  },
  projects: function (collectionApi) {
    return collectionApi.getFilteredByGlob('src/projects/**/*.md');
  }
};

Your eleventy.js now looks like this:

// Importing from config
const collections = require('./config/collections.js');

module.exports = eleventyConfig => {
  // Collections
  Object.keys(collections).forEach(collectionName => {
    eleventyConfig.addCollection(collectionName, collections[collectionName]);
  });

  return {
    dir: {
      input: 'src',
      output: 'dist',
      includes: '_includes',
      layouts: '_layouts'
    }
  };
};

We loop through all the collections that are defined in collections.js and import them into our config file. You’d now do exactly the same for your filters, transforms, shortcodes, etc.

If you want to see this method in action, visit the public repository of Ryan Mulligan’s personal site.

Very tidy!

This approach has the advantage of producing a really compact config file. There is something I don’t like about this method though.

We have brought structure into it, but I also want to see what’s being used in my project, right there, in my config file. I want to see which collections I’m defining, which filters, and so on. So here comes method two!

Method 2: named exports

Instead of collections.js create another folder inside config called collections, and in there you put a file called index.js:

// blog posts
const getPosts = collection => {
  return collection.getFilteredByGlob('src/posts/**/*.md');
};

// projects
const getProjects = collection => {
  return collection.getFilteredByGlob('src/projects/**/*.md');
};

module.exports = {
  getPosts,
  getProjects
};

Named exports can be exported individually, or batched together and exported at the bottom of the module. Exporting everything at the bottom, like in the example here, is so much cleaner, so I naturally favor this method.

And inside your eleventy.js:

// Importing from config
const {getPosts, getProjects} = require('./config/collections/index.js');

module.exports = eleventyConfig => {
  // Collections
  eleventyConfig.addCollection('posts', getPosts);
  eleventyConfig.addCollection('projects', getProjects);

  return {
    dir: {
      input: 'src',
      output: 'dist',
      includes: '_includes',
      layouts: '_layouts'
    }
  };
};

Done!

Everything is neat and I can see at a glance what I am importing for this project.

If there are too many filters, collections or shortcodes, I subdivide them further into their own folders, for example only the filters for handling the date in a common place. Larger blocks like the ones for the Eleventy Image shortcodes get their very own folder.
The exported values are then imported into the parent index.js, and then exported all together again for the eleventy.js file. 🤪

Method 3: adding another config file as a plugin

After I shared this article on Mastodon, Zach pointed out that there is yet another way to outsource my config components:

eleventyConfig.addPlugin(require('other-config-file.js'));

Not only is this the most compact notation, as I don’t have to import my return values first, but I also don’t have to remodel any code, as these outsourced config-files work just the same as eleventy.js, returning a callback function. And I can see what I am importing!

I illustrate this with the example of an html minification using Eleventy’s built-in addTransform.

In your eleventy.js:

// nothing to import! :)

module.exports = eleventyConfig => {
  // Transforms
  eleventyConfig.addPlugin(require('./config/transforms/html-config.js'));

  return {
    dir: {
      input: 'src',
      output: 'dist',
      includes: '_includes',
      layouts: '_layouts'
    }
  };
};

Your html-config.js:

const htmlmin = require('html-minifier-terser');
const isProduction = process.env.ELEVENTY_ENV === 'production';

module.exports = eleventyConfig => {
  eleventyConfig.addTransform('html-minify', (content, path) => {
    if (path && path.endsWith('.html') && isProduction) {
      return htmlmin.minify(content, {
        collapseBooleanAttributes: true,
        collapseWhitespace: true,
        decodeEntities: true,
        includeAutoGeneratedTags: false,
        removeComments: true
      });
    }

    return content;
  });
};

Excellent!

Next up: Passthrough File Copy.

Structuring your Passthrough File Copies

Sometimes we just want to copy files over to our output folder, without subjecting them to further transformation processes. Exactly as they are. This is how Passthrough File Copies come into play.

Keep directory structure intact

Let’s say you have stored your local fonts in src/assets/fonts.

If you want to keep the same nesting structure, you add the following to eleventy.js:

module.exports = eleventyConfig => {
  // Passthrough Copy
  eleventyConfig.addPassthroughCopy('src/assets/fonts/');

  return {
    dir: {
      input: 'src',
      output: 'dist',
      includes: '_includes',
      layouts: '_layouts'
    }
  };
};

Now your fonts will be copied over with the same directory structure, in dist/assets/fonts/.

I usually have more than one folder in assets that I want to deal with in the same way.
There is a concise way for this too!

['src/assets/fonts/', 'src/assets/images/', 'src/assets/pdf/'].forEach(path =>
  eleventyConfig.addPassthroughCopy(path)
);

We put all directories into an array and apply the forEach() method to execute the passthrough once for each array element.

Copy files to another directory

Maybe you want to copy your files to another directory. For me, this makes especially sense for my favicon variants. You can tell the browser to look for them inside a folder, but my experience has been that they’re best put in the root directory of the web page. However, I don’t want to see them in my project root (too much noise!), so I usually put them all in src/assets/images/favicon/.

To copy a single file over to the dist root directory, use this snippet:

eleventyConfig.addPassthroughCopy({
  'src/assets/images/favicon/apple-touch-icon.png': 'apple-touch-icon.png'
});

Now you could do this for every favicon file, but that would be a lot of unnecessary repetition. Instead, you can select all the files in the favicon directory with the * (asterisk) wildcard:

eleventyConfig.addPassthroughCopy({
  'src/assets/images/favicon/*': '/'
});

By the way, regarding favicons, I recommend reading Andrey Sitnik’s article about which variants you really need.

More Passthrough File Copy tricks

You can get really creative here.

You can address any JPG-image in any directoy, and send them alltogether to a common output folder:

eleventyConfig.addPassthroughCopy({
  '**/*.jpg': 'img'
});

You can combine your assets in an array, keeping the directory structure:

['src/assets/images/', 'src/assets/fonts/', 'src/assets/pdf/'].forEach(path =>
  eleventyConfig.addPassthroughCopy(path)
);

Or apply this interesting filtering that I discovered in the source code of Robb Knight’s personal website, where everything inside src/assets and src/files is being copied to the output directory, except any CSS files and files starting with an underscore.

['src/assets', 'src/files'].forEach(path => {
  eleventyConfig.addPassthroughCopy(path, {
    filter: path => !path.endsWith('.css') && !path.startsWith('_')
  });
});

Wrap up

This is how I’m currenlty structuring my projects. You can see this being applied in my starter eleventy-excellent. A wonderful example of a tidy Eleventy config file can be found in the repository of Miriam Suzanne’s personal website.

A look at the official Eleventy starter is always worthwhile, because there you can find cutting edge ideas from Zach, the creator of Eleventy.

Generally, it is always a great idea to dive deeply into the repositories of starter projects or personal sites of other developers.

There are so many great ideas out there!

Eleventy Meetup

I was invited as a speaker at the Eleventy Meetup Ep. 12 on March 16, 2023 and gave a short talk based on this article.


I try to keep my articles up to date, and of course I could be wrong, or there could be a better solution. If you see something that is not true (anymore), or something that should be mentioned, feel free to edit the article on GitHub.

Webmentions

Have you published a response? Let me know where: