Find out how I customize Eleventy to achieve my opinionated requirements about the structure of source files, output files and URLs.
Eleventy offers zero configuration (file structure and URLs) out-of-the-box. It adopts the concept of Cool URIs don't change - the generated URLs will follow its filename by default, with no file extensions. For example, if you have an about-me.html
source file, the URL for it would be /about-me/
.
This is great but not exactly what I want because:
- I have a slightly different folder setup.
- Some of my files follow specific naming conventions.
- That means URL might be different from its source file name.
- I don't like how the output files are structured by default.
Feel free to dive into the source code jec-11ty-starter straight away. This is the 3rd post of the series. Here is the previous post - Setting up GitHub Actions and Firebase Hosting.
Organizing source files
I organized all source files under subfolders. General pages like /licenses, /blog listing, /404, even the home page are placed under a folder named root
.
For content pages like blog posts and presentation decks, they will be organized:
- under the folder of its content category
- each content source file name will be prefixed with ISO created date (YYYY-MM-DD), for source file sorting purposes.
For instance, the "Web Performance Optimization" presentation is placed under the deck
folder and the file name would be "2019-08-31-web-performance-optimization.md".
Below is an overview of how source files are structured:
# Source file structure
src
- root
- index.njk
- licenses.njk
- blog.njk
- blog
- 2020-05-19-post-one.md
In future, I might add in a new content category named read
. I will then add a read.njk
in the root
folder and create a read
folder for all my reading reviews.
* If you are wondering about the file extensions here, I use markdown .md
file for content writing and Nunjucks .njk
file for general pages that need more html manipulation.
How does 11ty process our source files?
Eleventy will process our source files (both .njk
and .md
) into .html
pages by default. Base on the above file structure, this would be the output:-
# Output file structure by default
dist
- root
- index.html
- licenses
- index.html
- blog
- index.html
- blog
- 2020-05-19-post-one
- index.html
Each source file will output 2 items:
- a folder with the same name as its source file
- an
index.html
file in that folder
.
Based on the above output files, you can probably guess the website URLs! Here is an overview of the URLs:
# Website URLs by default
- /root/
- /root/licenses/
- /root/blog/
- /blog/2020-05-19-post-one/
Erm... that doesn't look like what I want.
What is my expected output?
There is nothing wrong with the default output, 11ty processes our files naively based on our input structure by default. However, that is not what I want.
Here are my requirements:
- Each source file should output just ONE HTML file, no folder.
- NO DATE information in the URLs.
- URLs should be CLEAN. (Explain in a bit)
Here is the output I'd like:-
# My expected output
dist
- index.html
- licenses.html
- blog.html
- deck.html
- blog
- post-one.html # no date
What do I mean by "URLs should be clean"? I prefer URLs with no trailing slash and no file extension. For example:-
- đ /about-me
- đ /about-me/
- âšī¸ /about-me.html
I prefer the first URL format. However, with the above output files, when the user browses to licenses
page, for example, she will land on /licenses.html, oops! đĨ
No worryies though, we've got that covered in the previous post. We have updated the server settings (Firebase cleanURLs
) to eliminate file extensions when users browse to our page, so /licenses
it is. No file extension, no trailing slash. đ
So what we need to do now is play with 11ty's configurations to generate the above output.
TLDR: Does website URLs matter? Trailing slash, file extension or date.
No, it doesn't impact the searchability (aka SEO) of your site, and users might not even care about or notice that.
But! It matters to me. This is my site, the trailing slash and extension hurt my eyes. đ I don't want date information in URLs because it is not meaningful (I might update the content from time to time).
Coincidentally, my colleague - Jake did a Twitter poll on the same topic few days ago. đ Mathias replied on it as well. My preference (and the majority) is same as Mathias: prefer links without trailing slash.
What are the solutions then?
Now that we understand the requirements, let's solve that! After reading the documentation multiple times and performing various testing. I found a few ways to achieve the desired result. Let us go through it one by one, from basic to pro. đ
First pass: Adding permalink in each file
It is quite easy to change the output filename. We just need to update the permalink
on the file's Front Matter Data
Let's look at our licenses.njk
file as an example.
<!-- root/licenses.njk -->
---
title: Licenses
permalink: licenses.html
---
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
Hello licenses page.
</body>
</html>
The ---
top section is something we called Front Matter Data (briefly mentioned in 1st post). 11ty wil preprocess the Front Matter Data before the template.
There are some built-in Front Matter Data that we can use. Permalink is one of them, and it is a special one (further explained later) because permalink
field gives us a way to change the output file format and location. In our case, src/root/licenses.njk
will now output as dist/licenses.html
, without root
in it and no extra folder!
More information on Front Matter Data is available in the documentation.
Second pass: Use 11ty supplied data in permalink
Editing permalink in file manually is prone to typing errors. We can improve our manual typing slightly by utilizing some of the Eleventy Supplied Data. Here is the list of supplied data that we can use.
We will be using the page.filePathStem supplied data. It provides us the full filename - without the date prefix and extension.
Here are the filePathStem
value for each source files:
source file | filePathStem |
---|---|
root/index.njk | /root/index |
root/licenses.njk | /root/licenses |
root/blog.njk | /root/blog |
blog/2020-05-19-post-one.md | /blog/post-one |
Look at the blog post, the date information is gone. One more thing we need to solve though. For the general files, we want the output to be without /root
. We achieve that by using the Nunjucks built-in replace filter.
Let's update our permalink
to use the page.filePathStem
:-
<!-- root/licenses.njk -->
---
permalink: "{{ page.filePathStem | replace('/root/', '/') }}.html"
---
...
There you go. Remember I mentioned earlier that permalink
is a special one? It is because it allows us to write code. The logic will be interpolated in later stages.
Third pass: Setting permalink per directory
Well, both the first and second pass require us to update each file manually, so it is time-wasting! Is there a better way? Let's go up one level, where we can set the permalink
data per directory by creating a Directory Data File per folder.
Here is the new file structure.
# File structure
- src
- root
- root.11tydata.js # add this
- index.njk
- licenses.njk
- blog.njk
- blog
- blog.11tydata.js # add this
- 2020-05-19-post-one.njk
Please note that the Directory Data file name:
- Must be the same as the directory name
- Must ends with
.11tydata.js
(you configure it)
Okay, let's update both of our Directory Data files.
// root/root.11tydata.js
// blog/blog.11tydata.js
module.exports = {
permalink: "{{page.filePathStem | replace('/root/', '/')}}.html"
};
It's okay for us to use the same code. In fact, you can shorten the permalink
in blog.11ty.data.js
to just {{page.filePathStem}}.html
if you want to, but I prefer to use the same logic.
With this setting, each file under the same directory will get the permalink
value injected automatically. We don't need to update every file manually.
In case you have a file in the directory that needs a special permalink
, you can still override it using the Front Matter Data
in each file. For example, let's say we add a RSS file in the root
folder atom.njk
. We want the output to be index.xml
- no /root
in name and the file extension should be xml
.
Here is how we can do it. đđŧ The priority in the file Front Matter Data
is higher than the Directiry Template Data
and hence value get overridden.
// root/atom.njk
module.exports = {
permalink: index.xml
};
Final pass: Do it once, setting permalink globally
Still, I wasn't happy with the per-directory approach, as you probably do too. I want to find a way to do it just once and forget about it.
After reading the documentation - real world example and data precedence a few times, and through various testing, I found a way to do it.
11ty accepts a global data folder _data
(customizable). We can place our global data or function here (will talk more about this in the coming post).
There is one special file we can add into this folder - the Computed Data File
. Name it as eleventyComputed.js
(must be this name). Let's create the folder and file.
# file structure
# add _data folder and `eleventyComputed.js`
- src
- _data
- eleventyComputed.js
...
Here is the code for our eleventyComputed.js
file.
// _data/eleventyComputed.js
module.exports = {
permalink:
'{% set p = page.filePathStem | replace("/root/", "/") %}' +
'{{ permalink or (p + ".html)" }}'
};
Notes for Nunjucks newbies:
- Nunjucks uses
{% %}
as the template syntax. - Nunjucks uses
{{ }}
for interpolation. - The built-in set tag is used to create/modify a variable.
The code is slightly different from our Data Directory Data
, we add an or
statement here:-
- if
permalink
already exists, we will not override the value - if
permalink
is empty, we will populate that with our value
Basically, the permalink
value in the global _data/eleventyComputed.js
directory has the highest priority of all. It will override the value in all Data Directory Data
and Front Matter Data
files. Refer to the advanced details section in the documentation.
We don't want the permalink
to be overridden if we have set it somewhere else for a special case (e.g. the RSS file). Therefore, we add an or
statement to check that.
Voila! No more per file nor per directory settings. Just one global computed data to rule it all. đ
One more thing... configure our localhost
If you run npm start
to serve the project now, it will show a "page not found" error when you browse to /licenses page. However, it works when you browse to /licenses.html.
What happened is that we configured our production server (Firebase Hosting) to handle the cleanURLs
(eliminate .html
), but we have not configured our local server yet.
Eleventy uses Browsersync under the hood to serve our local files. The good news is we can customize that too! Let's configure that in our .eleventy.js
configuration file.
// .eleventy.js
module.exports = function (eleventyConfig) {
// Browsersync config
eleventyConfig.setBrowserSyncConfig(
// will add our configuration code here
);
...
}
The configuration code is slightly lengthy, so I placed that in a separate file configs/browsersync.config.js. (I might consider creating an npm package in future, maybe!)
// configs/browsersync.config.js
const fs = require('fs');
const url = require('url');
module.exports = (output) => ({
server: {
baseDir: `${output}`,
middleware: [
function (req, res, next) {
let file = url.parse(req.url);
file = file.pathname;
file = file.replace(/\/+$/, ''); // remove trailing hash
file = `${output}/${file}.html`;
if (fs.existsSync(file)) {
const content = fs.readFileSync(file);
res.write(content);
res.writeHead(200);
res.end();
} else {
return next();
}
},
],
},
callbacks: {
ready: function (_, bs) {
bs.addMiddleware('*', (_, res) => {
const content = fs.readFileSync(`${output}/404.html`);
res.write(content);
res.writeHead(404);
res.end();
});
},
},
});
No worries, it's okay to skip reading these code, just use it! đ Next, update our .eleventy.js
file to use that.
// .eleventy.js
module.exports = function (eleventyConfig) {
// Browsersync config
eleventyConfig.setBrowserSyncConfig(
// add this line - dist is our output directory
require('./configs/browsersync.config')('dist')
);
...
}
Alrighty, what's next?
Yay! We have set up our file structure, cleaned the URLs and configure Browsersync to support that. đ We have learned about Front Matter Data
, Data File Directory
Computed Data
, the global folder and Browsersync too.
That's how my site jec.fish was set up as well.
TLDR; Does these effort worth it?
Actually... you might not need this. Seriously. Just go with the default settings and you will be happy, hah!
I am opionated as I have mentioned. I am surprised that I can go so far and even write a whole blog post about this, basically just to:-
- make my output file structure look good
- make the URLs look good
(No offense all, good in my definition! đ)
Nevertheless, I enjoy the process of exploration and testing the flexibility of Eleventy. Turns out, there is a lot of customization you can do with it.
In the coming posts, I plan to write about more on how I built my website with 11ty:
- Building Personal Static Site with Eleventy â
- Setting up GitHub Actions and Firebase Hosting â
- Customizing File Structure, URLs and Browsersync â
- Automating Image Optimization Workflow â
- Setting up SEO and Google Analytics â
- Minifying HTML, JavaScript, CSS - Automate Inline â
- How many favicons should you have in your site? â
- Creating Filters, Shortcodes and Plugins â
- Supporting Dark Mode in Your Website â
- and probably more!
Let me know if the above topics interest you.
Here's the GitHub repo for the code above: jec-11ty-starter. I'll update the repo whenever I write a new post.
That's all. Happy coding!
Have something to say? Leave me comments on Twitter đđŧ
Follow my writing: @jecfish