Published on: Jul 21, 2020, by Young-Suk Ahn Park
Check the Fundamenty project live, or try it locally by cloning this repo.
I first tried Eleventy for my personal website about a month ago, and it became my Static Site Generator (SSG) of choice.
Previously at my work, I had evaluated several other SSGs: Docsify, Docusaurs, Gatsby, GitBook, Hugo, mdBook, and mkdocs. The purpose of the evaluation was to choose a tool to generate technical documentation for our code repose, following the philosophy of Docs as Code. I ended up choosing mkdocs, mainly because of the short learning curve.
Eleventy was the only one on top 10 in SSG (at the moment of evaluation) that I hadn’t tried before, primarily because it hasn’t reached version 1.0 yet (it is still on version 0.11.0 at the moment of this writing). Nonetheless, I started bumping into articles praising Eleventy.
A month ago I decided to migrate my personal page that was running on a PHP-based CMS to a GitLab Pages (Yes, free hosting! Thanks GitLab for saving me $20/month on hosting). I needed a static page, and mkdocs or any other documentation-oriented SSG such as nkBook and gitbook would not do the job, its template is too restrictive. I hesitated in between Gatsby, Next with static site generation, but ultimately decided to give Eleventy a try.
I enjoyed it a lot!
It is very flexible, it supports multiple template engines; it is extensible, you can just use any JS library; it is simple and elegant.
Once I finished with my personal I had two other sites in the queue, including this one.
Few requirements I had for the sites are:
I looked for Eleventy Starter Project that fits - or approximates - my requirements. I found projects with pieces but not one that met all the requirements, so I went ahead and built one: Fundamenty! Combining the words Fun, Fundamental and Eleventy!
The gist:
In addition, I included
In this post, I will go over the implementation of top two features.
Besides Eleventy 0.11, Fundamenty uses TailwindCSS as CSS framework and Webpack for bundling.
If not explicitly mentioned, I will be using Nunjuck template for Elventy.
For the Eleventy and Tailwind setup, I followed this nice article from statickit.
Then I had to add the following styles for the tags generated by markdown:
h1 {
@apply leading-relaxed text-4xl text-teal-800 font-bold;
}
h2 {
@apply leading-relaxed text-3xl text-teal-600;
}
h3 {
@apply leading-relaxed text-2xl text-teal-600;
}
h3 {
@apply leading-relaxed text-lg text-teal-600;
}
p {
@apply leading-relaxed my-3
}
ul {
@apply text-base pl-8 list-disc;
}
ol {
@apply text-base pl-8 list-decimal;
}
table {
@apply text-base border-gray-600;
}
th {
@apply border py-1 px-3;
}
td {
@apply border py-1 px-3;
}
blockquote {
@apply border m-1 p-2 bg-gray-200;
}
a {
@apply underline;
}
a:hover {
@apply underline text-blue-500;
}
Because the style applies not only to the markdown generated elements but for all other elements in the layout, I had to override those that, e.g. the navigation menu uses <ul>, but shouldn’t be rendered with bullets, I had to add list-none
.
I have not found a good way to add a class to all elements generated from markdown. I would need something like markdown-it-attrs but that includes a class for all elements by default.
I also configured tailwind.config.js to enable css purge for production build
module.exports = {
purge: {
enabled: (process.env.ELEVENTY_ENV === 'production'),
content: [
'./src/**/*.njk',
'./src/**/*.md'
]
},
...
}
For Eleventy and Webpack configuration, I followed this nice guid from statickit.
Then I added support for dotenv by modifying webpack.config.js:
var dotenv = require('dotenv').config({ path: __dirname + '/.env' });
...
module.exports = {
...
plugins: [
new MiniCssExtractPlugin(),
new webpack.DefinePlugin({
"process.env": JSON.stringify(process.env)
})
],
...
};
The most natural way of implementing multi-language support is through directory per language (I will be using language and locale interchangeably).
All the source code is under ./src
directory. The multi-language relevant files and directories are shown below:
├───{lang} - Contents in given locale
│ ├ {lang}.json - Common front matter for all the contents in Spanish
│ ├───pages - Site pages
│ └───posts - Site posts (e.g. blog articles)
├───_data - Data/configuration file.
│ ├ site.js - Main site configuration data.
│ ├───l10n - localization resource bundles
Under _data directory, the default 11ty’s data directory, includes sites.js data file that contains definition of list of active languages and resource bundles.
const site = {
// properties for Branding
// Active languages
langs: [{
"id": "es",
"name": "Español"
}, {
"id": "en",
"name": "English"
}],
// locale message bundle:
// _t is for translation
en: {
menu: require("./l10n/menu_en.json"),
_t: require("./l10n/messages_en.json")
},
es: {
menu: require("./l10n/menu_es.json"),
_t: require("./l10n/messages_es.json")
},
};
module.exports = site;
Notice that the resource bundles and messages were externalized into separate files, they are just json files located under ./src/_data/l10n
.
Each language directory includes corresponding data file, with two entries: layout and locale, for example the English directory en
includes the en.json
file
{
"layout": "/layout/base.njk",
"locale": "en"
}
Now, in the base template, you would define the language of the page
<html lang="en">
And provide links for all active languages
{% for lang in site.langs %}
<li >
<a href="{{ ('/' + lang.id) | url }}" >{{lang.name}}</a>
</li>
{% endfor %}
Then anywhere in the template you can access the menu by locale
{% for menuItem in site[locale].menu.top %}
...
{% endfor %}
And get the translation by locale
{{ site[locale]._t.search }}
To create a content, all you need to do is to create a file, usually markdown file with front matter in the corresponding language directory.
The directory under language can be arbitrary, but the starter project includes pages
and posts
sub-directories. The former, posts
, has the nature of increasing over time.
You will notice that any given collection will include contents of all languages. For example, collection.post
will include, say English and Spanish.
In order to have language specific collections, .eleventy.js
was modified to add configuration that adds collections per language. For example from collection.post will derive collection.posts_en
and collection.posts_es
.
site.langs.map(langEntry => {
eleventyConfig.addCollection(`posts_${langEntry.id}`, function(collectionApi) {
return collectionApi.getFilteredByTag("post").filter(function(item) {
return item.data.locale === langEntry.id
});
});
});
This allows you to render English only posts. If you want to parameterize the language, you could do:
{% set posts = collections['posts_' + locale] %}
I wish I could just access with collections.posts[locale]
.
In the midst of implementing my awesome multilingual Eleventy, I found out this nice article about multilingual sites, to which happened to use very similar techniques. The permalink idea in the article looks interesting.
✔️ Requirement: Multilingual - Checked!
The objective of Search Engine Optimization (SEO) is to make the page such a way that the search engines such as Google, Bing, etc. can parse it and index it with useful information increasing the chance of the page to be found by people.
SEO involves many things including proper meta tagging: proper wording for the description, appropriate keywords, good title; the proper usage of headings, etc. I am not an expert, but there is a nice tool from WooRank that analyses your page. There is also a free chrome extension that does that.
One feature I have for Fundamenty is the generation of robots.txt
and sitemap.xml
The generation is done through the template files ./src/robots-txt.njk
and ./src/sitemap-xml.njk
.
Both templates are quite simple
./src/robots-txt.njk
---
permalink: /robots.txt
eleventyExcludeFromCollections: true
---
Sitemap: https://creasoft-dev.github.io//sitemap.xml
User-agent: *
Disallow:
And ./src/sitemap-xml.njk
---
permalink: /sitemap.xml
eleventyExcludeFromCollections: true
---
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for page in collections.all %}{% if not page.data.sitemapExclude %}
<url>
<loc>{{ site.rootUrl }}{{ page.url | url }}</loc>
<lastmod>{{ page.date.toISOString() }}</lastmod>
<changefreq>{{page.data.changeFreq}}</changefreq>
</url>
{% endif %}{% endfor %}
</urlset>
Both relies on the rootUrl
data defined in site.js
if (process.env.WEB_ROOT_URL) {
site.rootUrl = process.env.WEB_ROOT_URL;
}
✔️ Requirement: Basic SEO - Checked!
In next articles, I will explain how I addressed my other requirements: search, analytics, automated deployment (continuous deployment) and others.
Published on: Jul 21, 2020, by Young-Suk Ahn Park
Edit on Git